mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-23 23:57:23 +01:00
chore: add biome for linting/formatting + CI action for linting in SRC folder (#1284)
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
Code quality / quality (push) Waiting to run
* 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
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
export const last = <T>(array: T[]): T => {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get unique elements from an array
|
||||
|
|
@ -14,7 +14,7 @@ export const last = <T>(array: T[]): T => {
|
|||
*/
|
||||
export const unique = <T>(array: T[]): T[] => {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deep copy an object or array
|
||||
|
|
@ -24,12 +24,15 @@ export const unique = <T>(array: T[]): T[] => {
|
|||
export const deepCopy = <T>(obj: T): T => {
|
||||
const id = (x: T): T => x;
|
||||
const dcTArray = (a: T[]): T[] => a.map(id);
|
||||
const dcObject = (x: object): object => Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
|
||||
const dcAny = (x: any): any => (x instanceof Object ? (cf.get(x.constructor) || id)(x) : x);
|
||||
const dcObject = (x: object): object =>
|
||||
Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
|
||||
const dcAny = (x: any): any =>
|
||||
x instanceof Object ? (cf.get(x.constructor) || id)(x) : x;
|
||||
// don't map keys, probably this is what we would expect
|
||||
const dcMapCore = (m: Map<any, any>): [any, any][] => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
|
||||
const dcMapCore = (m: Map<any, any>): [any, any][] =>
|
||||
[...m.entries()].map(([k, v]) => [k, dcAny(v)]);
|
||||
|
||||
const cf: Map<Function, (x: any) => any> = new Map<any, (x: any) => any>([
|
||||
const cf: Map<any, (x: any) => any> = new Map<any, (x: any) => any>([
|
||||
[Int8Array, dcTArray],
|
||||
[Uint8Array, dcTArray],
|
||||
[Uint8ClampedArray, dcTArray],
|
||||
|
|
@ -41,17 +44,17 @@ export const deepCopy = <T>(obj: T): T => {
|
|||
[Float64Array, dcTArray],
|
||||
[BigInt64Array, dcTArray],
|
||||
[BigUint64Array, dcTArray],
|
||||
[Map, m => new Map(dcMapCore(m))],
|
||||
[WeakMap, m => new WeakMap(dcMapCore(m))],
|
||||
[Array, a => a.map(dcAny)],
|
||||
[Set, s => [...s.values()].map(dcAny)],
|
||||
[Date, d => new Date(d.getTime())],
|
||||
[Object, dcObject]
|
||||
[Map, (m) => new Map(dcMapCore(m))],
|
||||
[WeakMap, (m) => new WeakMap(dcMapCore(m))],
|
||||
[Array, (a) => a.map(dcAny)],
|
||||
[Set, (s) => [...s.values()].map(dcAny)],
|
||||
[Date, (d) => new Date(d.getTime())],
|
||||
[Object, dcObject],
|
||||
// ... extend here to implement their custom deep copy
|
||||
]);
|
||||
|
||||
return dcAny(obj);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the appropriate typed array constructor based on the maximum value
|
||||
|
|
@ -60,15 +63,17 @@ export const deepCopy = <T>(obj: T): T => {
|
|||
*/
|
||||
export const getTypedArray = (maxValue: number) => {
|
||||
console.assert(
|
||||
Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= TYPED_ARRAY_MAX_VALUES.UINT32_MAX,
|
||||
`Array maxValue must be an integer between 0 and ${TYPED_ARRAY_MAX_VALUES.UINT32_MAX}, got ${maxValue}`
|
||||
Number.isInteger(maxValue) &&
|
||||
maxValue >= 0 &&
|
||||
maxValue <= TYPED_ARRAY_MAX_VALUES.UINT32_MAX,
|
||||
`Array maxValue must be an integer between 0 and ${TYPED_ARRAY_MAX_VALUES.UINT32_MAX}, got ${maxValue}`,
|
||||
);
|
||||
|
||||
if (maxValue <= TYPED_ARRAY_MAX_VALUES.UINT8_MAX) return Uint8Array;
|
||||
if (maxValue <= TYPED_ARRAY_MAX_VALUES.UINT16_MAX) return Uint16Array;
|
||||
if (maxValue <= TYPED_ARRAY_MAX_VALUES.UINT32_MAX) return Uint32Array;
|
||||
return Uint32Array;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a typed array based on the maximum value and length or from an existing array
|
||||
|
|
@ -78,18 +83,26 @@ 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<number>}): Uint8Array | Uint16Array | Uint32Array => {
|
||||
export const createTypedArray = ({
|
||||
maxValue,
|
||||
length,
|
||||
from,
|
||||
}: {
|
||||
maxValue: number;
|
||||
length: number;
|
||||
from?: ArrayLike<number>;
|
||||
}): Uint8Array | Uint16Array | Uint32Array => {
|
||||
const typedArray = getTypedArray(maxValue);
|
||||
if (!from) return new typedArray(length);
|
||||
return typedArray.from(from);
|
||||
}
|
||||
};
|
||||
|
||||
// typed arrays max values
|
||||
export const TYPED_ARRAY_MAX_VALUES = {
|
||||
INT8_MAX: 127,
|
||||
UINT8_MAX: 255,
|
||||
UINT16_MAX: 65535,
|
||||
UINT32_MAX: 4294967295
|
||||
UINT32_MAX: 4294967295,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import { color, interpolate, interpolateRainbow, range, RGBColor, scaleSequential, shuffler } from "d3";
|
||||
import {
|
||||
color,
|
||||
interpolate,
|
||||
interpolateRainbow,
|
||||
type RGBColor,
|
||||
range,
|
||||
scaleSequential,
|
||||
shuffler,
|
||||
} from "d3";
|
||||
|
||||
/**
|
||||
* Convert RGB or RGBA color to HEX
|
||||
|
|
@ -8,14 +16,16 @@ import { color, interpolate, interpolateRainbow, range, RGBColor, scaleSequentia
|
|||
export const toHEX = (rgba: string): string => {
|
||||
if (rgba.charAt(0) === "#") return rgba;
|
||||
|
||||
const matches = rgba.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
||||
const matches = rgba.match(
|
||||
/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i,
|
||||
);
|
||||
return matches && matches.length === 4
|
||||
? "#" +
|
||||
("0" + parseInt(matches[1], 10).toString(16)).slice(-2) +
|
||||
("0" + parseInt(matches[2], 10).toString(16)).slice(-2) +
|
||||
("0" + parseInt(matches[3], 10).toString(16)).slice(-2)
|
||||
`0${parseInt(matches[1], 10).toString(16)}`.slice(-2) +
|
||||
`0${parseInt(matches[2], 10).toString(16)}`.slice(-2) +
|
||||
`0${parseInt(matches[3], 10).toString(16)}`.slice(-2)
|
||||
: "";
|
||||
}
|
||||
};
|
||||
|
||||
/** Predefined set of 12 distinct colors */
|
||||
export const C_12 = [
|
||||
|
|
@ -30,33 +40,39 @@ export const C_12 = [
|
|||
"#ccebc5",
|
||||
"#ffed6f",
|
||||
"#8dd3c7",
|
||||
"#eb8de7"
|
||||
"#eb8de7",
|
||||
];
|
||||
|
||||
/**
|
||||
/**
|
||||
* Get an array of distinct colors
|
||||
* Uses shuffler with current Math.random to ensure seeded randomness works
|
||||
* @param {number} count - The count of colors to generate
|
||||
* @returns {string[]} - The array of HEX color strings
|
||||
*/
|
||||
*/
|
||||
export const getColors = (count: number): string[] => {
|
||||
const scaleRainbow = scaleSequential(interpolateRainbow);
|
||||
// Use shuffler() to create a shuffle function that uses the current Math.random
|
||||
const shuffle = shuffler(() => Math.random());
|
||||
const colors = shuffle(
|
||||
range(count).map(i => (i < 12 ? C_12[i] : color(scaleRainbow((i - 12) / (count - 12)))?.formatHex()))
|
||||
range(count).map((i) =>
|
||||
i < 12
|
||||
? C_12[i]
|
||||
: color(scaleRainbow((i - 12) / (count - 12)))?.formatHex(),
|
||||
),
|
||||
);
|
||||
return colors.filter((c): c is string => typeof c === "string");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a random color in HEX format
|
||||
* @returns {string} - The HEX color string
|
||||
*/
|
||||
export const getRandomColor = (): string => {
|
||||
const colorFromRainbow: RGBColor = color(scaleSequential(interpolateRainbow)(Math.random())) as RGBColor;
|
||||
const colorFromRainbow: RGBColor = color(
|
||||
scaleSequential(interpolateRainbow)(Math.random()),
|
||||
) as RGBColor;
|
||||
return colorFromRainbow.formatHex();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a mixed color by blending a given color with a random color
|
||||
|
|
@ -65,11 +81,17 @@ export const getRandomColor = (): string => {
|
|||
* @param {number} bright - The brightness adjustment
|
||||
* @returns {string} - The mixed HEX color string
|
||||
*/
|
||||
export const getMixedColor = (colorToMix: string, mix = 0.2, bright = 0.3): string => {
|
||||
export const getMixedColor = (
|
||||
colorToMix: string,
|
||||
mix = 0.2,
|
||||
bright = 0.3,
|
||||
): string => {
|
||||
const c = colorToMix && colorToMix[0] === "#" ? colorToMix : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
|
||||
const mixedColor: RGBColor = color(interpolate(c, getRandomColor())(mix)) as RGBColor;
|
||||
const mixedColor: RGBColor = color(
|
||||
interpolate(c, getRandomColor())(mix),
|
||||
) as RGBColor;
|
||||
return mixedColor.brighter(bright).formatHex();
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -78,5 +100,5 @@ declare global {
|
|||
getRandomColor: typeof getRandomColor;
|
||||
getMixedColor: typeof getMixedColor;
|
||||
C_12: typeof C_12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import { expect, describe, it } from 'vitest'
|
||||
import { getLongitude, getLatitude, getCoordinates } from './commonUtils'
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getCoordinates, getLatitude, getLongitude } from "./commonUtils";
|
||||
|
||||
describe('getLongitude', () => {
|
||||
describe("getLongitude", () => {
|
||||
const mapCoordinates = { lonW: -10, lonT: 20 };
|
||||
const graphWidth = 1000;
|
||||
|
||||
it('should calculate longitude at the left edge (x=0)', () => {
|
||||
it("should calculate longitude at the left edge (x=0)", () => {
|
||||
expect(getLongitude(0, mapCoordinates, graphWidth, 2)).toBe(-10);
|
||||
});
|
||||
|
||||
it('should calculate longitude at the right edge (x=graphWidth)', () => {
|
||||
it("should calculate longitude at the right edge (x=graphWidth)", () => {
|
||||
expect(getLongitude(1000, mapCoordinates, graphWidth, 2)).toBe(10);
|
||||
});
|
||||
|
||||
it('should calculate longitude at the center (x=graphWidth/2)', () => {
|
||||
it("should calculate longitude at the center (x=graphWidth/2)", () => {
|
||||
expect(getLongitude(500, mapCoordinates, graphWidth, 2)).toBe(0);
|
||||
});
|
||||
|
||||
it('should respect decimal precision', () => {
|
||||
it("should respect decimal precision", () => {
|
||||
// 333/1000 * 20 = 6.66, -10 + 6.66 = -3.34
|
||||
expect(getLongitude(333, mapCoordinates, graphWidth, 4)).toBe(-3.34);
|
||||
});
|
||||
|
||||
it('should handle different map coordinate ranges', () => {
|
||||
it("should handle different map coordinate ranges", () => {
|
||||
const wideMap = { lonW: -180, lonT: 360 };
|
||||
expect(getLongitude(500, wideMap, graphWidth, 2)).toBe(0);
|
||||
expect(getLongitude(0, wideMap, graphWidth, 2)).toBe(-180);
|
||||
|
|
@ -30,68 +30,109 @@ describe('getLongitude', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getLatitude', () => {
|
||||
describe("getLatitude", () => {
|
||||
const mapCoordinates = { latN: 60, latT: 40 };
|
||||
const graphHeight = 800;
|
||||
|
||||
it('should calculate latitude at the top edge (y=0)', () => {
|
||||
it("should calculate latitude at the top edge (y=0)", () => {
|
||||
expect(getLatitude(0, mapCoordinates, graphHeight, 2)).toBe(60);
|
||||
});
|
||||
|
||||
it('should calculate latitude at the bottom edge (y=graphHeight)', () => {
|
||||
it("should calculate latitude at the bottom edge (y=graphHeight)", () => {
|
||||
expect(getLatitude(800, mapCoordinates, graphHeight, 2)).toBe(20);
|
||||
});
|
||||
|
||||
it('should calculate latitude at the center (y=graphHeight/2)', () => {
|
||||
it("should calculate latitude at the center (y=graphHeight/2)", () => {
|
||||
expect(getLatitude(400, mapCoordinates, graphHeight, 2)).toBe(40);
|
||||
});
|
||||
|
||||
it('should respect decimal precision', () => {
|
||||
it("should respect decimal precision", () => {
|
||||
// 60 - (333/800 * 40) = 60 - 16.65 = 43.35
|
||||
expect(getLatitude(333, mapCoordinates, graphHeight, 4)).toBe(43.35);
|
||||
});
|
||||
|
||||
it('should handle equator-centered maps', () => {
|
||||
it("should handle equator-centered maps", () => {
|
||||
const equatorMap = { latN: 45, latT: 90 };
|
||||
expect(getLatitude(400, equatorMap, graphHeight, 2)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCoordinates', () => {
|
||||
describe("getCoordinates", () => {
|
||||
const mapCoordinates = { lonW: -10, lonT: 20, latN: 60, latT: 40 };
|
||||
const graphWidth = 1000;
|
||||
const graphHeight = 800;
|
||||
|
||||
it('should return [longitude, latitude] tuple', () => {
|
||||
const result = getCoordinates(500, 400, mapCoordinates, graphWidth, graphHeight, 2);
|
||||
it("should return [longitude, latitude] tuple", () => {
|
||||
const result = getCoordinates(
|
||||
500,
|
||||
400,
|
||||
mapCoordinates,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
2,
|
||||
);
|
||||
expect(result).toEqual([0, 40]);
|
||||
});
|
||||
|
||||
it('should calculate coordinates at top-left corner', () => {
|
||||
const result = getCoordinates(0, 0, mapCoordinates, graphWidth, graphHeight, 2);
|
||||
it("should calculate coordinates at top-left corner", () => {
|
||||
const result = getCoordinates(
|
||||
0,
|
||||
0,
|
||||
mapCoordinates,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
2,
|
||||
);
|
||||
expect(result).toEqual([-10, 60]);
|
||||
});
|
||||
|
||||
it('should calculate coordinates at bottom-right corner', () => {
|
||||
const result = getCoordinates(1000, 800, mapCoordinates, graphWidth, graphHeight, 2);
|
||||
it("should calculate coordinates at bottom-right corner", () => {
|
||||
const result = getCoordinates(
|
||||
1000,
|
||||
800,
|
||||
mapCoordinates,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
2,
|
||||
);
|
||||
expect(result).toEqual([10, 20]);
|
||||
});
|
||||
|
||||
it('should respect decimal precision for both coordinates', () => {
|
||||
const result = getCoordinates(333, 333, mapCoordinates, graphWidth, graphHeight, 4);
|
||||
it("should respect decimal precision for both coordinates", () => {
|
||||
const result = getCoordinates(
|
||||
333,
|
||||
333,
|
||||
mapCoordinates,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
4,
|
||||
);
|
||||
expect(result[0]).toBe(-3.34); // longitude
|
||||
expect(result[1]).toBe(43.35); // latitude
|
||||
});
|
||||
|
||||
it('should use default precision of 2 decimals', () => {
|
||||
const result = getCoordinates(333, 333, mapCoordinates, graphWidth, graphHeight);
|
||||
it("should use default precision of 2 decimals", () => {
|
||||
const result = getCoordinates(
|
||||
333,
|
||||
333,
|
||||
mapCoordinates,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
);
|
||||
expect(result[0]).toBe(-3.34);
|
||||
expect(result[1]).toBe(43.35);
|
||||
});
|
||||
|
||||
it('should handle global map coordinates', () => {
|
||||
it("should handle global map coordinates", () => {
|
||||
const globalMap = { lonW: -180, lonT: 360, latN: 90, latT: 180 };
|
||||
const result = getCoordinates(500, 400, globalMap, graphWidth, graphHeight, 2);
|
||||
const result = getCoordinates(
|
||||
500,
|
||||
400,
|
||||
globalMap,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
2,
|
||||
);
|
||||
expect(result).toEqual([0, 0]); // center of the world
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { distanceSquared } from "./functionUtils";
|
||||
import { rand } from "./probabilityUtils";
|
||||
import { rn } from "./numberUtils";
|
||||
import { last } from "./arrayUtils";
|
||||
import { distanceSquared } from "./functionUtils";
|
||||
import { rn } from "./numberUtils";
|
||||
import { rand } from "./probabilityUtils";
|
||||
|
||||
/**
|
||||
* Clip polygon points to graph boundaries
|
||||
|
|
@ -11,15 +11,20 @@ import { last } from "./arrayUtils";
|
|||
* @param secure - Secure clipping to avoid edge artifacts
|
||||
* @returns Clipped polygon points
|
||||
*/
|
||||
export const clipPoly = (points: [number, number][], graphWidth?: number, graphHeight?: number, secure: number = 0) => {
|
||||
export const clipPoly = (
|
||||
points: [number, number][],
|
||||
graphWidth?: number,
|
||||
graphHeight?: number,
|
||||
secure: number = 0,
|
||||
) => {
|
||||
if (points.length < 2) return points;
|
||||
if (points.some(point => point === undefined)) {
|
||||
if (points.some((point) => point === undefined)) {
|
||||
window.ERROR && console.error("Undefined point in clipPoly", points);
|
||||
return points;
|
||||
}
|
||||
|
||||
return window.polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get segment of any point on polyline
|
||||
|
|
@ -28,7 +33,11 @@ export const clipPoly = (points: [number, number][], graphWidth?: number, graphH
|
|||
* @param step - Step size for segment search (default is 10)
|
||||
* @returns The segment ID (1-indexed)
|
||||
*/
|
||||
export const getSegmentId = (points: [number, number][], point: [number, number], step: number = 10): number => {
|
||||
export const getSegmentId = (
|
||||
points: [number, number][],
|
||||
point: [number, number],
|
||||
step: number = 10,
|
||||
): number => {
|
||||
if (points.length === 2) return 1;
|
||||
|
||||
let minSegment = 1;
|
||||
|
|
@ -55,7 +64,7 @@ export const getSegmentId = (points: [number, number][], point: [number, number]
|
|||
}
|
||||
|
||||
return minSegment;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking func until after ms milliseconds have elapsed
|
||||
|
|
@ -63,16 +72,21 @@ export const getSegmentId = (points: [number, number][], point: [number, number]
|
|||
* @param ms - The number of milliseconds to delay
|
||||
* @returns The debounced function
|
||||
*/
|
||||
export const debounce = <T extends (...args: any[]) => any>(func: T, ms: number) => {
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
ms: number,
|
||||
) => {
|
||||
let isCooldown = false;
|
||||
|
||||
return function (this: any, ...args: Parameters<T>) {
|
||||
if (isCooldown) return;
|
||||
func.apply(this, args);
|
||||
isCooldown = true;
|
||||
setTimeout(() => (isCooldown = false), ms);
|
||||
setTimeout(() => {
|
||||
isCooldown = false;
|
||||
}, ms);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a throttled function that only invokes func at most once every ms milliseconds
|
||||
|
|
@ -80,7 +94,10 @@ export const debounce = <T extends (...args: any[]) => any>(func: T, ms: number)
|
|||
* @param ms - The number of milliseconds to throttle invocations to
|
||||
* @returns The throttled function
|
||||
*/
|
||||
export const throttle = <T extends (...args: any[]) => any>(func: T, ms: number) => {
|
||||
export const throttle = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
ms: number,
|
||||
) => {
|
||||
let isThrottled = false;
|
||||
let savedArgs: any[] | null = null;
|
||||
let savedThis: any = null;
|
||||
|
|
@ -95,7 +112,7 @@ export const throttle = <T extends (...args: any[]) => any>(func: T, ms: number)
|
|||
func.apply(this, args);
|
||||
isThrottled = true;
|
||||
|
||||
setTimeout(function () {
|
||||
setTimeout(() => {
|
||||
isThrottled = false;
|
||||
if (savedArgs) {
|
||||
wrapper.apply(savedThis, savedArgs as Parameters<T>);
|
||||
|
|
@ -105,7 +122,7 @@ export const throttle = <T extends (...args: any[]) => any>(func: T, ms: number)
|
|||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse error to get the readable string in Chrome and Firefox
|
||||
|
|
@ -114,23 +131,32 @@ export const throttle = <T extends (...args: any[]) => any>(func: T, ms: number)
|
|||
*/
|
||||
export const parseError = (error: Error): string => {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
|
||||
const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack || "";
|
||||
const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
|
||||
const errorNoURL = errorString.replace(regex, url => "<i>" + last(url.split("/")) + "</i>");
|
||||
const errorString = isFirefox
|
||||
? `${error.toString()} ${error.stack}`
|
||||
: error.stack || "";
|
||||
const regex =
|
||||
/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;
|
||||
const errorNoURL = errorString.replace(
|
||||
regex,
|
||||
(url) => `<i>${last(url.split("/"))}</i>`,
|
||||
);
|
||||
const errorParsed = errorNoURL.replace(/at /gi, "<br> at ");
|
||||
return errorParsed;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a URL to base64 encoded data
|
||||
* @param url - The URL to convert
|
||||
* @param callback - Callback function that receives the base64 data
|
||||
*/
|
||||
export const getBase64 = (url: string, callback: (result: string | ArrayBuffer | null) => void): void => {
|
||||
export const getBase64 = (
|
||||
url: string,
|
||||
callback: (result: string | ArrayBuffer | null) => void,
|
||||
): void => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onload = function () {
|
||||
xhr.onload = () => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
reader.onloadend = () => {
|
||||
callback(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(xhr.response);
|
||||
|
|
@ -138,7 +164,7 @@ export const getBase64 = (url: string, callback: (result: string | ArrayBuffer |
|
|||
xhr.open("GET", url);
|
||||
xhr.responseType = "blob";
|
||||
xhr.send();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open URL in a new tab or window
|
||||
|
|
@ -146,15 +172,18 @@ export const getBase64 = (url: string, callback: (result: string | ArrayBuffer |
|
|||
*/
|
||||
export const openURL = (url: string): void => {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open project wiki-page
|
||||
* @param page - The wiki page name/path to open
|
||||
*/
|
||||
export const wiki = (page: string): void => {
|
||||
window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
|
||||
}
|
||||
window.open(
|
||||
`https://github.com/Azgaar/Fantasy-Map-Generator/wiki/${page}`,
|
||||
"_blank",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap URL into html a element
|
||||
|
|
@ -164,7 +193,7 @@ export const wiki = (page: string): void => {
|
|||
*/
|
||||
export const link = (URL: string, description: string): string => {
|
||||
return `<a href="${URL}" rel="noopener" target="_blank">${description}</a>`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if Ctrl key (or Cmd on Mac) was pressed during an event
|
||||
|
|
@ -174,7 +203,7 @@ export const link = (URL: string, description: string): string => {
|
|||
export const isCtrlClick = (event: MouseEvent | KeyboardEvent): boolean => {
|
||||
// meta key is cmd key on MacOs
|
||||
return event.ctrlKey || event.metaKey;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a random date within a specified range
|
||||
|
|
@ -186,9 +215,9 @@ export const generateDate = (from: number = 100, to: number = 1000): string => {
|
|||
return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert x coordinate to longitude
|
||||
|
|
@ -198,9 +227,17 @@ export const generateDate = (from: number = 100, to: number = 1000): string => {
|
|||
* @param decimals - Number of decimal places (default is 2)
|
||||
* @returns Longitude value
|
||||
*/
|
||||
export const getLongitude = (x: number, mapCoordinates: any, graphWidth: number, decimals: number = 2): number => {
|
||||
return rn(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, decimals);
|
||||
}
|
||||
export const getLongitude = (
|
||||
x: number,
|
||||
mapCoordinates: any,
|
||||
graphWidth: number,
|
||||
decimals: number = 2,
|
||||
): number => {
|
||||
return rn(
|
||||
mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT,
|
||||
decimals,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert y coordinate to latitude
|
||||
|
|
@ -210,9 +247,17 @@ export const getLongitude = (x: number, mapCoordinates: any, graphWidth: number,
|
|||
* @param decimals - Number of decimal places (default is 2)
|
||||
* @returns Latitude value
|
||||
*/
|
||||
export const getLatitude = (y: number, mapCoordinates: any, graphHeight: number, decimals: number = 2): number => {
|
||||
return rn(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, decimals);
|
||||
}
|
||||
export const getLatitude = (
|
||||
y: number,
|
||||
mapCoordinates: any,
|
||||
graphHeight: number,
|
||||
decimals: number = 2,
|
||||
): number => {
|
||||
return rn(
|
||||
mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT,
|
||||
decimals,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert x,y coordinates to longitude,latitude
|
||||
|
|
@ -224,9 +269,19 @@ export const getLatitude = (y: number, mapCoordinates: any, graphHeight: number,
|
|||
* @param decimals - Number of decimal places (default is 2)
|
||||
* @returns Array with [longitude, latitude]
|
||||
*/
|
||||
export const getCoordinates = (x: number, y: number, mapCoordinates: any, graphWidth: number, graphHeight: number, decimals: number = 2): [number, number] => {
|
||||
return [getLongitude(x, mapCoordinates, graphWidth, decimals), getLatitude(y, mapCoordinates, graphHeight, decimals)];
|
||||
}
|
||||
export const getCoordinates = (
|
||||
x: number,
|
||||
y: number,
|
||||
mapCoordinates: any,
|
||||
graphWidth: number,
|
||||
graphHeight: number,
|
||||
decimals: number = 2,
|
||||
): [number, number] => {
|
||||
return [
|
||||
getLongitude(x, mapCoordinates, graphWidth, decimals),
|
||||
getLatitude(y, mapCoordinates, graphHeight, decimals),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt options interface
|
||||
|
|
@ -246,22 +301,39 @@ export interface PromptOptions {
|
|||
export const initializePrompt = (): void => {
|
||||
const prompt = document.getElementById("prompt");
|
||||
if (!prompt) return;
|
||||
|
||||
|
||||
const form = prompt.querySelector("#promptForm");
|
||||
if (!form) return;
|
||||
|
||||
const defaultText = "Please provide an input";
|
||||
const defaultOptions: PromptOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
|
||||
const defaultOptions: PromptOptions = {
|
||||
default: 1,
|
||||
step: 0.01,
|
||||
min: 0,
|
||||
max: 100,
|
||||
required: true,
|
||||
};
|
||||
|
||||
(window as any).prompt = function (promptText: string = defaultText, options: PromptOptions = defaultOptions, callback?: (value: number | string) => void) {
|
||||
(window as any).prompt = (
|
||||
promptText: string = defaultText,
|
||||
options: PromptOptions = defaultOptions,
|
||||
callback?: (value: number | string) => void,
|
||||
) => {
|
||||
if (options.default === undefined)
|
||||
return window.ERROR && console.error("Prompt: options object does not have default value defined");
|
||||
return (
|
||||
window.ERROR &&
|
||||
console.error(
|
||||
"Prompt: options object does not have default value defined",
|
||||
)
|
||||
);
|
||||
|
||||
const input = prompt.querySelector("#promptInput") as HTMLInputElement;
|
||||
const promptTextElement = prompt.querySelector("#promptText") as HTMLElement;
|
||||
|
||||
const promptTextElement = prompt.querySelector(
|
||||
"#promptText",
|
||||
) as HTMLElement;
|
||||
|
||||
if (!input || !promptTextElement) return;
|
||||
|
||||
|
||||
promptTextElement.innerHTML = promptText;
|
||||
|
||||
const type = typeof options.default === "number" ? "number" : "text";
|
||||
|
|
@ -271,8 +343,8 @@ export const initializePrompt = (): void => {
|
|||
if (options.min !== undefined) input.min = options.min.toString();
|
||||
if (options.max !== undefined) input.max = options.max.toString();
|
||||
|
||||
input.required = options.required === false ? false : true;
|
||||
input.placeholder = "type a " + type;
|
||||
input.required = options.required !== false;
|
||||
input.placeholder = `type a ${type}`;
|
||||
input.value = options.default.toString();
|
||||
input.style.width = promptText.length > 10 ? "100%" : "auto";
|
||||
prompt.style.display = "block";
|
||||
|
|
@ -285,7 +357,7 @@ export const initializePrompt = (): void => {
|
|||
const v = type === "number" ? +input.value : input.value;
|
||||
if (callback) callback(v);
|
||||
},
|
||||
{once: true}
|
||||
{ once: true },
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -295,13 +367,13 @@ export const initializePrompt = (): void => {
|
|||
prompt.style.display = "none";
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ERROR: boolean;
|
||||
polygonclip: any;
|
||||
|
||||
|
||||
clipPoly: typeof clipPoly;
|
||||
getSegmentId: typeof getSegmentId;
|
||||
debounce: typeof debounce;
|
||||
|
|
@ -319,7 +391,14 @@ declare global {
|
|||
}
|
||||
|
||||
// Global variables defined in main.js
|
||||
var mapCoordinates: { latT?: number; latN?: number; latS?: number; lonT?: number; lonW?: number; lonE?: number };
|
||||
var mapCoordinates: {
|
||||
latT?: number;
|
||||
latN?: number;
|
||||
latS?: number;
|
||||
lonT?: number;
|
||||
lonW?: number;
|
||||
lonE?: number;
|
||||
};
|
||||
var graphWidth: number;
|
||||
var graphHeight: number;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {curveBundle, line, max, min} from "d3";
|
||||
import { normalize } from "./numberUtils";
|
||||
import { getGridPolygon } from "./graphUtils";
|
||||
import { curveBundle, line, max, min } from "d3";
|
||||
import { C_12 } from "./colorUtils";
|
||||
import { getGridPolygon } from "./graphUtils";
|
||||
import { normalize } from "./numberUtils";
|
||||
import { round } from "./stringUtils";
|
||||
|
||||
/**
|
||||
|
|
@ -19,7 +19,7 @@ export const drawCellsValue = (data: any[], packedGraph: any): void => {
|
|||
.attr("x", (_d: any, i: number) => packedGraph.cells.p[i][0])
|
||||
.attr("y", (_d: any, i: number) => packedGraph.cells.p[i][1])
|
||||
.text((d: any) => d);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Drawing polygons colored according to data values for debugging purposes
|
||||
* @param {number[]} data - Array of numerical values corresponding to each cell
|
||||
|
|
@ -28,9 +28,11 @@ export const drawCellsValue = (data: any[], packedGraph: any): void => {
|
|||
export const drawPolygons = (data: number[], terrs: any, grid: any): void => {
|
||||
const maximum: number = max(data) as number;
|
||||
const minimum: number = min(data) as number;
|
||||
const scheme = window.getColorScheme(terrs.select("#landHeights").attr("scheme"));
|
||||
const scheme = window.getColorScheme(
|
||||
terrs.select("#landHeights").attr("scheme"),
|
||||
);
|
||||
|
||||
data = data.map(d => 1 - normalize(d, minimum, maximum));
|
||||
data = data.map((d) => 1 - normalize(d, minimum, maximum));
|
||||
window.debug.selectAll("polygon").remove();
|
||||
window.debug
|
||||
.selectAll("polygon")
|
||||
|
|
@ -40,7 +42,7 @@ export const drawPolygons = (data: number[], terrs: any, grid: any): void => {
|
|||
.attr("points", (_d: number, i: number) => getGridPolygon(i, grid))
|
||||
.attr("fill", (d: number) => scheme(d))
|
||||
.attr("stroke", (d: number) => scheme(d));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Drawing route connections for debugging purposes
|
||||
|
|
@ -48,7 +50,10 @@ export const drawPolygons = (data: number[], terrs: any, grid: any): void => {
|
|||
*/
|
||||
export const drawRouteConnections = (packedGraph: any): void => {
|
||||
window.debug.select("#connections").remove();
|
||||
const routes = window.debug.append("g").attr("id", "connections").attr("stroke-width", 0.8);
|
||||
const routes = window.debug
|
||||
.append("g")
|
||||
.attr("id", "connections")
|
||||
.attr("stroke-width", 0.8);
|
||||
|
||||
const points = packedGraph.cells.p;
|
||||
const links = packedGraph.cells.routes;
|
||||
|
|
@ -70,7 +75,7 @@ export const drawRouteConnections = (packedGraph: any): void => {
|
|||
.attr("stroke", C_12[routeId % 12]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Drawing a point for debugging purposes
|
||||
|
|
@ -79,9 +84,17 @@ export const drawRouteConnections = (packedGraph: any): void => {
|
|||
* @param {string} options.color - Color of the point
|
||||
* @param {number} options.radius - Radius of the point
|
||||
*/
|
||||
export const drawPoint = ([x, y]: [number, number], {color = "red", radius = 0.5}): void => {
|
||||
window.debug.append("circle").attr("cx", x).attr("cy", y).attr("r", radius).attr("fill", color);
|
||||
}
|
||||
export const drawPoint = (
|
||||
[x, y]: [number, number],
|
||||
{ color = "red", radius = 0.5 },
|
||||
): void => {
|
||||
window.debug
|
||||
.append("circle")
|
||||
.attr("cx", x)
|
||||
.attr("cy", y)
|
||||
.attr("r", radius)
|
||||
.attr("fill", color);
|
||||
};
|
||||
|
||||
/**
|
||||
* Drawing a path for debugging purposes
|
||||
|
|
@ -90,7 +103,10 @@ export const drawPoint = ([x, y]: [number, number], {color = "red", radius = 0.5
|
|||
* @param {string} options.color - Color of the path
|
||||
* @param {number} options.width - Stroke width of the path
|
||||
*/
|
||||
export const drawPath = (points: [number, number][], {color = "red", width = 0.5}): void => {
|
||||
export const drawPath = (
|
||||
points: [number, number][],
|
||||
{ color = "red", width = 0.5 },
|
||||
): void => {
|
||||
const lineGen = line().curve(curveBundle);
|
||||
window.debug
|
||||
.append("path")
|
||||
|
|
@ -98,17 +114,17 @@ export const drawPath = (points: [number, number][], {color = "red", width = 0.5
|
|||
.attr("stroke", color)
|
||||
.attr("stroke-width", width)
|
||||
.attr("fill", "none");
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
debug: any;
|
||||
getColorScheme: (name: string) => (t: number) => string;
|
||||
|
||||
|
||||
drawCellsValue: typeof drawCellsValue;
|
||||
drawPolygons: typeof drawPolygons;
|
||||
drawRouteConnections: typeof drawRouteConnections;
|
||||
drawPoint: typeof drawPoint;
|
||||
drawPath: typeof drawPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* @param {Function} reduce - The reduce function to apply to each group
|
||||
* @param {...Function} keys - The key functions to group by
|
||||
* @returns {Map} - The regrouped and reduced Map
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const data = [
|
||||
* {category: 'A', type: 'X', value: 10},
|
||||
|
|
@ -24,11 +24,20 @@
|
|||
* // 'B' => Map { 'X' => 30, 'Y' => 40 }
|
||||
* // }
|
||||
*/
|
||||
export const rollups = (values: any[], reduce: (values: any[]) => any, ...keys: ((value: any, index: number, array: any[]) => any)[]) => {
|
||||
export const rollups = (
|
||||
values: any[],
|
||||
reduce: (values: any[]) => any,
|
||||
...keys: ((value: any, index: number, array: any[]) => any)[]
|
||||
) => {
|
||||
return nest(values, Array.from, reduce, keys);
|
||||
}
|
||||
};
|
||||
|
||||
const nest = (values: any[], map: (iterable: Iterable<any>) => any, reduce: (values: any[]) => any, keys: ((value: any, index: number, array: any[]) => any)[]) => {
|
||||
const nest = (
|
||||
values: any[],
|
||||
map: (iterable: Iterable<any>) => any,
|
||||
reduce: (values: any[]) => any,
|
||||
keys: ((value: any, index: number, array: any[]) => any)[],
|
||||
) => {
|
||||
return (function regroup(values, i) {
|
||||
if (i >= keys.length) return reduce(values);
|
||||
const groups = new Map();
|
||||
|
|
@ -45,7 +54,7 @@ const nest = (values: any[], map: (iterable: Iterable<any>) => any, reduce: (val
|
|||
}
|
||||
return map(groups);
|
||||
})(values, 0);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate squared distance between two points
|
||||
|
|
@ -53,12 +62,15 @@ const nest = (values: any[], map: (iterable: Iterable<any>) => any, reduce: (val
|
|||
* @param {[number, number]} p2 - Second point [x2, y2]
|
||||
* @returns {number} - Squared distance between p1 and p2
|
||||
*/
|
||||
export const distanceSquared = ([x1, y1]: [number, number], [x2, y2]: [number, number]) => {
|
||||
export const distanceSquared = (
|
||||
[x1, y1]: [number, number],
|
||||
[x2, y2]: [number, number],
|
||||
) => {
|
||||
return (x1 - x2) ** 2 + (y1 - y2) ** 2;
|
||||
}
|
||||
};
|
||||
declare global {
|
||||
interface Window {
|
||||
rollups: typeof rollups;
|
||||
dist2: typeof distanceSquared;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import Delaunator from "delaunator";
|
||||
import Alea from "alea";
|
||||
import { color } from "d3";
|
||||
import { byId } from "./shorthands";
|
||||
import { rn } from "./numberUtils";
|
||||
import Delaunator from "delaunator";
|
||||
import {
|
||||
type Cells,
|
||||
type Point,
|
||||
type Vertices,
|
||||
Voronoi,
|
||||
} from "../modules/voronoi";
|
||||
import { createTypedArray } from "./arrayUtils";
|
||||
import { Cells, Vertices, Voronoi, Point } from "../modules/voronoi";
|
||||
import { rn } from "./numberUtils";
|
||||
import { byId } from "./shorthands";
|
||||
|
||||
/**
|
||||
* Get boundary points on a regular square grid
|
||||
|
|
@ -13,7 +18,11 @@ import { Cells, Vertices, Voronoi, Point } from "../modules/voronoi";
|
|||
* @param {number} spacing - The spacing between points
|
||||
* @returns {Array} - An array of boundary points
|
||||
*/
|
||||
const getBoundaryPoints = (width: number, height: number, spacing: number): Point[] => {
|
||||
const getBoundaryPoints = (
|
||||
width: number,
|
||||
height: number,
|
||||
spacing: number,
|
||||
): Point[] => {
|
||||
const offset = rn(-1 * spacing);
|
||||
const bSpacing = spacing * 2;
|
||||
const w = width - offset * 2;
|
||||
|
|
@ -23,17 +32,17 @@ const getBoundaryPoints = (width: number, height: number, spacing: number): Poin
|
|||
const points: Point[] = [];
|
||||
|
||||
for (let i = 0.5; i < numberX; i++) {
|
||||
let x = Math.ceil((w * i) / numberX + offset);
|
||||
const x = Math.ceil((w * i) / numberX + offset);
|
||||
points.push([x, offset], [x, h + offset]);
|
||||
}
|
||||
|
||||
for (let i = 0.5; i < numberY; i++) {
|
||||
let y = Math.ceil((h * i) / numberY + offset);
|
||||
const y = Math.ceil((h * i) / numberY + offset);
|
||||
points.push([offset, y], [w + offset, y]);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get points on a jittered square grid
|
||||
|
|
@ -42,13 +51,17 @@ const getBoundaryPoints = (width: number, height: number, spacing: number): Poin
|
|||
* @param {number} spacing - The spacing between points
|
||||
* @returns {Array} - An array of jittered grid points
|
||||
*/
|
||||
const getJitteredGrid = (width: number, height: number, spacing: number): Point[] => {
|
||||
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: Point[] = [];
|
||||
const 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);
|
||||
|
|
@ -57,7 +70,7 @@ const getJitteredGrid = (width: number, height: number, spacing: number): Point[
|
|||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Places points on a jittered grid and calculates spacing and cell counts
|
||||
|
|
@ -65,7 +78,17 @@ const getJitteredGrid = (width: number, height: number, spacing: number): Point[
|
|||
* @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): {spacing: number, cellsDesired: number, boundary: Point[], points: Point[], cellsX: number, cellsY: number} => {
|
||||
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 jittering
|
||||
|
|
@ -73,12 +96,20 @@ const placePoints = (graphWidth: number, graphHeight: number): {spacing: number,
|
|||
const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
|
||||
const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
|
||||
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
|
||||
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: cellCountX, cellsY: cellCountY};
|
||||
}
|
||||
|
||||
return {
|
||||
spacing,
|
||||
cellsDesired,
|
||||
boundary,
|
||||
points,
|
||||
cellsX: cellCountX,
|
||||
cellsY: cellCountY,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the grid needs to be regenerated based on desired parameters
|
||||
|
|
@ -88,18 +119,34 @@ const placePoints = (graphWidth: number, graphHeight: number): {spacing: number,
|
|||
* @param {number} graphHeight - The height of the graph
|
||||
* @returns {boolean} - True if the grid should be regenerated, false otherwise
|
||||
*/
|
||||
export const shouldRegenerateGrid = (grid: any, expectedSeed: number, graphWidth: number, graphHeight: number) => {
|
||||
export const shouldRegenerateGrid = (
|
||||
grid: any,
|
||||
expectedSeed: number,
|
||||
graphWidth: number,
|
||||
graphHeight: number,
|
||||
) => {
|
||||
if (expectedSeed && expectedSeed !== grid.seed) return true;
|
||||
|
||||
const cellsDesired = +(byId("pointsInput")?.dataset?.cells || 0);
|
||||
if (cellsDesired !== grid.cellsDesired) return true;
|
||||
|
||||
const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2);
|
||||
const newCellsX = Math.floor((graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing);
|
||||
const newCellsY = Math.floor((graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing);
|
||||
const newSpacing = rn(
|
||||
Math.sqrt((graphWidth * graphHeight) / cellsDesired),
|
||||
2,
|
||||
);
|
||||
const newCellsX = Math.floor(
|
||||
(graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing,
|
||||
);
|
||||
const newCellsY = Math.floor(
|
||||
(graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing,
|
||||
);
|
||||
|
||||
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
|
||||
}
|
||||
return (
|
||||
grid.spacing !== newSpacing ||
|
||||
grid.cellsX !== newCellsX ||
|
||||
grid.cellsY !== newCellsY
|
||||
);
|
||||
};
|
||||
|
||||
interface Grid {
|
||||
spacing: number;
|
||||
|
|
@ -116,12 +163,27 @@ interface Grid {
|
|||
* 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): Grid => {
|
||||
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);
|
||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed};
|
||||
}
|
||||
const { spacing, cellsDesired, boundary, points, cellsX, cellsY } =
|
||||
placePoints(graphWidth, graphHeight);
|
||||
const { cells, vertices } = calculateVoronoi(points, boundary);
|
||||
return {
|
||||
spacing,
|
||||
cellsDesired,
|
||||
boundary,
|
||||
points,
|
||||
cellsX,
|
||||
cellsY,
|
||||
cells,
|
||||
vertices,
|
||||
seed,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the Voronoi diagram from given points and boundary
|
||||
|
|
@ -129,7 +191,10 @@ 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: Point[], boundary: Point[]): {cells: Cells, vertices: Vertices} => {
|
||||
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);
|
||||
|
|
@ -139,12 +204,15 @@ export const calculateVoronoi = (points: Point[], boundary: Point[]): {cells: Ce
|
|||
const voronoi = new Voronoi(delaunay, allPoints, points.length);
|
||||
|
||||
const cells = voronoi.cells;
|
||||
cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i) as Uint32Array; // array of indexes
|
||||
cells.i = createTypedArray({
|
||||
maxValue: points.length,
|
||||
length: points.length,
|
||||
}).map((_, i) => i) as Uint32Array; // array of indexes
|
||||
const vertices = voronoi.vertices;
|
||||
TIME && console.timeEnd("calculateVoronoi");
|
||||
|
||||
return {cells, vertices};
|
||||
}
|
||||
return { cells, vertices };
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a cell index on a regular square grid based on x and y coordinates
|
||||
|
|
@ -158,9 +226,9 @@ export const findGridCell = (x: number, y: number, grid: any): number => {
|
|||
Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX +
|
||||
Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* return array of cell indexes in radius on a regular square grid
|
||||
* @param {number} x - The x coordinate
|
||||
* @param {number} y - The y coordinate
|
||||
|
|
@ -168,7 +236,12 @@ export const findGridCell = (x: number, y: number, grid: any): number => {
|
|||
* @param {Object} grid - The grid object containing spacing, cellsX, and cellsY
|
||||
* @returns {Array} - An array of cell indexes within the specified radius
|
||||
*/
|
||||
export const findGridAll = (x: number, y: number, radius: number, grid: any): number[] => {
|
||||
export const findGridAll = (
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
grid: any,
|
||||
): number[] => {
|
||||
const c = grid.cells.c;
|
||||
let r = Math.floor(radius / grid.spacing);
|
||||
let found = [findGridCell(x, y, grid)];
|
||||
|
|
@ -177,10 +250,10 @@ export const findGridAll = (x: number, y: number, radius: number, grid: any): nu
|
|||
if (r > 1) {
|
||||
let frontier = c[found[0]];
|
||||
while (r > 1) {
|
||||
let cycle = frontier.slice();
|
||||
const cycle = frontier.slice();
|
||||
frontier = [];
|
||||
cycle.forEach(function (s: number) {
|
||||
c[s].forEach(function (e: number) {
|
||||
cycle.forEach((s: number) => {
|
||||
c[s].forEach((e: number) => {
|
||||
if (found.indexOf(e) !== -1) return;
|
||||
found.push(e);
|
||||
frontier.push(e);
|
||||
|
|
@ -191,7 +264,7 @@ export const findGridAll = (x: number, y: number, radius: number, grid: any): nu
|
|||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the index of the packed cell containing the given x and y coordinates
|
||||
|
|
@ -200,11 +273,16 @@ export const findGridAll = (x: number, y: number, radius: number, grid: any): nu
|
|||
* @param {number} radius - The search radius (default is Infinity)
|
||||
* @returns {number|undefined} - The index of the found cell or undefined if not found
|
||||
*/
|
||||
export const findClosestCell = (x: number, y: number, radius = Infinity, packedGraph: any): number | undefined => {
|
||||
export const findClosestCell = (
|
||||
x: number,
|
||||
y: number,
|
||||
radius = Infinity,
|
||||
packedGraph: any,
|
||||
): number | undefined => {
|
||||
if (!packedGraph.cells?.q) return;
|
||||
const found = packedGraph.cells.q.find(x, y, radius);
|
||||
return found ? found[2] : undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches a quadtree for all points within a given radius
|
||||
|
|
@ -215,21 +293,31 @@ export const findClosestCell = (x: number, y: number, radius = Infinity, packedG
|
|||
* @param {Object} quadtree - The D3 quadtree to search
|
||||
* @returns {Array} - An array of found data points within the radius
|
||||
*/
|
||||
export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree: any) => {
|
||||
export const findAllInQuadtree = (
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
quadtree: any,
|
||||
) => {
|
||||
let dx: number, dy: number, d2: number;
|
||||
|
||||
const radiusSearchInit = (t: any, radius: number) => {
|
||||
t.result = [];
|
||||
(t.x0 = t.x - radius), (t.y0 = t.y - radius);
|
||||
(t.x3 = t.x + radius), (t.y3 = t.y + radius);
|
||||
t.x0 = t.x - radius;
|
||||
t.y0 = t.y - radius;
|
||||
t.x3 = t.x + radius;
|
||||
t.y3 = t.y + radius;
|
||||
t.radius = radius * radius;
|
||||
};
|
||||
|
||||
const radiusSearchVisit = (t: any, d2: number) => {
|
||||
t.node.data.scanned = true;
|
||||
if (d2 < t.radius) {
|
||||
do {
|
||||
while (t.node) {
|
||||
t.result.push(t.node.data);
|
||||
t.node.data.selected = true;
|
||||
} while ((t.node = t.node.next));
|
||||
t.node = t.node.next;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -248,39 +336,52 @@ export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree
|
|||
}
|
||||
}
|
||||
|
||||
const t: any = {x, y, x0: quadtree._x0, y0: quadtree._y0, x3: quadtree._x1, y3: quadtree._y1, quads: [], node: quadtree._root};
|
||||
const t: any = {
|
||||
x,
|
||||
y,
|
||||
x0: quadtree._x0,
|
||||
y0: quadtree._y0,
|
||||
x3: quadtree._x1,
|
||||
y3: quadtree._y1,
|
||||
quads: [],
|
||||
node: quadtree._root,
|
||||
};
|
||||
if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
|
||||
radiusSearchInit(t, radius);
|
||||
|
||||
var i = 0;
|
||||
while ((t.q = t.quads.pop())) {
|
||||
i++;
|
||||
var _i = 0;
|
||||
t.q = t.quads.pop();
|
||||
while (t.q) {
|
||||
_i++;
|
||||
|
||||
t.node = t.q.node;
|
||||
t.x1 = t.q.x0;
|
||||
t.y1 = t.q.y0;
|
||||
t.x2 = t.q.x1;
|
||||
t.y2 = t.q.y1;
|
||||
|
||||
// Stop searching if this quadrant can't contain a closer node.
|
||||
if (
|
||||
!(t.node = t.q.node) ||
|
||||
(t.x1 = t.q.x0) > t.x3 ||
|
||||
(t.y1 = t.q.y0) > t.y3 ||
|
||||
(t.x2 = t.q.x1) < t.x0 ||
|
||||
(t.y2 = t.q.y1) < t.y0
|
||||
)
|
||||
if (!t.node || t.x1 > t.x3 || t.y1 > t.y3 || t.x2 < t.x0 || t.y2 < t.y0) {
|
||||
t.q = t.quads.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bisect the current quadrant.
|
||||
if (t.node.length) {
|
||||
t.node.explored = true;
|
||||
var xm: number = (t.x1 + t.x2) / 2,
|
||||
const xm: number = (t.x1 + t.x2) / 2,
|
||||
ym: number = (t.y1 + t.y2) / 2;
|
||||
|
||||
t.quads.push(
|
||||
new Quad(t.node[3], xm, ym, t.x2, t.y2),
|
||||
new Quad(t.node[2], t.x1, ym, xm, t.y2),
|
||||
new Quad(t.node[1], xm, t.y1, t.x2, ym),
|
||||
new Quad(t.node[0], t.x1, t.y1, xm, ym)
|
||||
new Quad(t.node[0], t.x1, t.y1, xm, ym),
|
||||
);
|
||||
|
||||
// Visit the closest quadrant first.
|
||||
if ((t.i = (+(y >= ym) << 1) | +(x >= xm))) {
|
||||
t.i = (+(y >= ym) << 1) | +(x >= xm);
|
||||
if (t.i) {
|
||||
t.q = t.quads[t.quads.length - 1];
|
||||
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
|
||||
t.quads[t.quads.length - 1 - t.i] = t.q;
|
||||
|
|
@ -289,14 +390,15 @@ export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree
|
|||
|
||||
// Visit this point. (Visiting coincident points isn't necessary!)
|
||||
else {
|
||||
var dx = x - +quadtree._x.call(null, t.node.data),
|
||||
dy = y - +quadtree._y.call(null, t.node.data),
|
||||
d2 = dx * dx + dy * dy;
|
||||
dx = x - +quadtree._x.call(null, t.node.data);
|
||||
dy = y - +quadtree._y.call(null, t.node.data);
|
||||
d2 = dx * dx + dy * dy;
|
||||
radiusSearchVisit(t, d2);
|
||||
}
|
||||
t.q = t.quads.pop();
|
||||
}
|
||||
return t.result;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of packed cell indexes within a specified radius from given x and y coordinates
|
||||
|
|
@ -306,11 +408,16 @@ export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree
|
|||
* @param {Object} packedGraph - The packed graph containing cells with quadtree
|
||||
* @returns {number[]} - An array of cell indexes within the radius
|
||||
*/
|
||||
export const findAllCellsInRadius = (x: number, y: number, radius: number, packedGraph: any): number[] => {
|
||||
export const findAllCellsInRadius = (
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
packedGraph: any,
|
||||
): number[] => {
|
||||
// Use findAllInQuadtree directly instead of relying on prototype extension
|
||||
const found = findAllInQuadtree(x, y, radius, packedGraph.cells.q);
|
||||
return found.map((r: any) => r[2]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the polygon points for a packed cell given its index
|
||||
|
|
@ -318,8 +425,10 @@ export const findAllCellsInRadius = (x: number, y: number, radius: number, packe
|
|||
* @returns {Array} - An array of polygon points for the specified cell
|
||||
*/
|
||||
export const getPackPolygon = (cellIndex: number, packedGraph: any) => {
|
||||
return packedGraph.cells.v[cellIndex].map((v: number) => packedGraph.vertices.p[v]);
|
||||
}
|
||||
return packedGraph.cells.v[cellIndex].map(
|
||||
(v: number) => packedGraph.vertices.p[v],
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the polygon points for a grid cell given its index
|
||||
|
|
@ -328,7 +437,7 @@ export const getPackPolygon = (cellIndex: number, packedGraph: any) => {
|
|||
*/
|
||||
export const getGridPolygon = (i: number, grid: any) => {
|
||||
return grid.cells.v[i].map((v: number) => grid.vertices.p[v]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* mbostock's poissonDiscSampler implementation
|
||||
|
|
@ -341,7 +450,14 @@ export const getGridPolygon = (i: number, grid: any) => {
|
|||
* @param {number} k - The number of attempts before rejection (default is 3)
|
||||
* @yields {Array} - An array containing the x and y coordinates of a generated point
|
||||
*/
|
||||
export function* poissonDiscSampler(x0: number, y0: number, x1: number, y1: number, r: number, k = 3) {
|
||||
export function* poissonDiscSampler(
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
r: number,
|
||||
k = 3,
|
||||
) {
|
||||
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
|
||||
|
||||
const width = x1 - x0;
|
||||
|
|
@ -377,7 +493,8 @@ export function* poissonDiscSampler(x0: number, y0: number, x1: number, y1: numb
|
|||
|
||||
function sample(x: number, y: number) {
|
||||
const point: [number, number] = [x, y];
|
||||
queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = point));
|
||||
grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = point;
|
||||
queue.push(point);
|
||||
return [x + x0, y + y0];
|
||||
}
|
||||
|
||||
|
|
@ -410,7 +527,7 @@ export function* poissonDiscSampler(x0: number, y0: number, x1: number, y1: numb
|
|||
*/
|
||||
export const isLand = (i: number, packedGraph: any) => {
|
||||
return packedGraph.cells.h[i] >= 20;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a packed cell is water based on its height
|
||||
|
|
@ -419,8 +536,7 @@ export const isLand = (i: number, packedGraph: any) => {
|
|||
*/
|
||||
export const isWater = (i: number, packedGraph: any) => {
|
||||
return packedGraph.cells.h[i] < 20;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// draw raster heightmap preview (not used in main generation)
|
||||
/**
|
||||
|
|
@ -433,18 +549,31 @@ export const isWater = (i: number, packedGraph: any) => {
|
|||
* @param {boolean} options.renderOcean - Whether to render ocean heights
|
||||
* @returns {string} - A data URL representing the drawn heightmap image
|
||||
*/
|
||||
export const drawHeights = ({heights, width, height, scheme, renderOcean}: {heights: number[], width: number, height: number, scheme: (value: number) => string, renderOcean: boolean}) => {
|
||||
export const drawHeights = ({
|
||||
heights,
|
||||
width,
|
||||
height,
|
||||
scheme,
|
||||
renderOcean,
|
||||
}: {
|
||||
heights: number[];
|
||||
width: number;
|
||||
height: number;
|
||||
scheme: (value: number) => string;
|
||||
renderOcean: boolean;
|
||||
}) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const imageData = ctx.createImageData(width, height);
|
||||
|
||||
const getHeight = (height: number) => (height < 20 ? (renderOcean ? height : 0) : height);
|
||||
const getHeight = (height: number) =>
|
||||
height < 20 ? (renderOcean ? height : 0) : height;
|
||||
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
const colorScheme = scheme(1 - getHeight(heights[i]) / 100);
|
||||
const {r, g, b} = color(colorScheme)!.rgb();
|
||||
const { r, g, b } = color(colorScheme)?.rgb() ?? { r: 0, g: 0, b: 0 };
|
||||
|
||||
const n = i * 4;
|
||||
imageData.data[n] = r;
|
||||
|
|
@ -455,12 +584,11 @@ export const drawHeights = ({heights, width, height, scheme, renderOcean}: {heig
|
|||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
var TIME: boolean;
|
||||
interface Window {
|
||||
|
||||
shouldRegenerateGrid: typeof shouldRegenerateGrid;
|
||||
generateGrid: typeof generateGrid;
|
||||
findCell: typeof findClosestCell;
|
||||
|
|
@ -476,4 +604,4 @@ declare global {
|
|||
findAllInQuadtree: typeof findAllInQuadtree;
|
||||
drawHeights: typeof drawHeights;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
import "./polyfills";
|
||||
|
||||
import { rn, lim, minmax, normalize, lerp } from "./numberUtils";
|
||||
import { lerp, lim, minmax, normalize, rn } from "./numberUtils";
|
||||
|
||||
window.rn = rn;
|
||||
window.lim = lim;
|
||||
window.minmax = minmax;
|
||||
window.normalize = normalize;
|
||||
window.lerp = lerp as typeof window.lerp;
|
||||
|
||||
import { isVowel, trimVowels, getAdjective, nth, abbreviate, list } from "./languageUtils";
|
||||
import {
|
||||
abbreviate,
|
||||
getAdjective,
|
||||
isVowel,
|
||||
list,
|
||||
nth,
|
||||
trimVowels,
|
||||
} from "./languageUtils";
|
||||
|
||||
window.vowel = isVowel;
|
||||
window.trimVowels = trimVowels;
|
||||
window.getAdjective = getAdjective;
|
||||
|
|
@ -15,7 +24,15 @@ window.nth = nth;
|
|||
window.abbreviate = abbreviate;
|
||||
window.list = list;
|
||||
|
||||
import { last, unique, deepCopy, getTypedArray, createTypedArray, TYPED_ARRAY_MAX_VALUES } from "./arrayUtils";
|
||||
import {
|
||||
createTypedArray,
|
||||
deepCopy,
|
||||
getTypedArray,
|
||||
last,
|
||||
TYPED_ARRAY_MAX_VALUES,
|
||||
unique,
|
||||
} from "./arrayUtils";
|
||||
|
||||
window.last = last;
|
||||
window.unique = unique;
|
||||
window.deepCopy = deepCopy;
|
||||
|
|
@ -26,7 +43,19 @@ window.UINT8_MAX = TYPED_ARRAY_MAX_VALUES.UINT8_MAX;
|
|||
window.UINT16_MAX = TYPED_ARRAY_MAX_VALUES.UINT16_MAX;
|
||||
window.UINT32_MAX = TYPED_ARRAY_MAX_VALUES.UINT32_MAX;
|
||||
|
||||
import { rand, P, each, gauss, Pint, biased, generateSeed, getNumberInRange, ra, rw } from "./probabilityUtils";
|
||||
import {
|
||||
biased,
|
||||
each,
|
||||
gauss,
|
||||
generateSeed,
|
||||
getNumberInRange,
|
||||
P,
|
||||
Pint,
|
||||
ra,
|
||||
rand,
|
||||
rw,
|
||||
} from "./probabilityUtils";
|
||||
|
||||
window.rand = rand;
|
||||
window.P = P;
|
||||
window.each = each;
|
||||
|
|
@ -38,12 +67,23 @@ window.biased = biased;
|
|||
window.getNumberInRange = getNumberInRange;
|
||||
window.generateSeed = generateSeed;
|
||||
|
||||
import { convertTemperature, si, getIntegerFromSI } from "./unitUtils";
|
||||
window.convertTemperature = (temp:number, scale: any = (window as any).temperatureScale.value || "°C") => convertTemperature(temp, scale);
|
||||
import { convertTemperature, getIntegerFromSI, si } from "./unitUtils";
|
||||
|
||||
window.convertTemperature = (
|
||||
temp: number,
|
||||
scale: any = (window as any).temperatureScale.value || "°C",
|
||||
) => convertTemperature(temp, scale);
|
||||
window.si = si;
|
||||
window.getInteger = getIntegerFromSI;
|
||||
|
||||
import { toHEX, getColors, getRandomColor, getMixedColor, C_12 } from "./colorUtils";
|
||||
import {
|
||||
C_12,
|
||||
getColors,
|
||||
getMixedColor,
|
||||
getRandomColor,
|
||||
toHEX,
|
||||
} from "./colorUtils";
|
||||
|
||||
window.toHEX = toHEX;
|
||||
window.getColors = getColors;
|
||||
window.getRandomColor = getRandomColor;
|
||||
|
|
@ -51,21 +91,41 @@ window.getMixedColor = getMixedColor;
|
|||
window.C_12 = C_12;
|
||||
|
||||
import { getComposedPath, getNextId } from "./nodeUtils";
|
||||
|
||||
window.getComposedPath = getComposedPath;
|
||||
window.getNextId = getNextId;
|
||||
|
||||
import { rollups, distanceSquared } from "./functionUtils";
|
||||
import { distanceSquared, rollups } from "./functionUtils";
|
||||
|
||||
window.rollups = rollups;
|
||||
window.dist2 = distanceSquared;
|
||||
|
||||
import { getIsolines, getPolesOfInaccessibility, connectVertices, findPath, getVertexPath } from "./pathUtils";
|
||||
import {
|
||||
connectVertices,
|
||||
findPath,
|
||||
getIsolines,
|
||||
getPolesOfInaccessibility,
|
||||
getVertexPath,
|
||||
} from "./pathUtils";
|
||||
|
||||
window.getIsolines = getIsolines;
|
||||
window.getPolesOfInaccessibility = getPolesOfInaccessibility;
|
||||
window.connectVertices = connectVertices;
|
||||
window.findPath = (start, end, getCost) => findPath(start, end, getCost, (window as any).pack);
|
||||
window.getVertexPath = (cellsArray) => getVertexPath(cellsArray, (window as any).pack);
|
||||
window.findPath = (start, end, getCost) =>
|
||||
findPath(start, end, getCost, (window as any).pack);
|
||||
window.getVertexPath = (cellsArray) =>
|
||||
getVertexPath(cellsArray, (window as any).pack);
|
||||
|
||||
import {
|
||||
capitalize,
|
||||
isValidJSON,
|
||||
parseTransform,
|
||||
round,
|
||||
safeParseJSON,
|
||||
sanitizeId,
|
||||
splitInTwo,
|
||||
} from "./stringUtils";
|
||||
|
||||
import { round, capitalize, splitInTwo, parseTransform, isValidJSON, safeParseJSON, sanitizeId } from "./stringUtils";
|
||||
window.round = round;
|
||||
window.capitalize = capitalize;
|
||||
window.splitInTwo = splitInTwo;
|
||||
|
|
@ -76,6 +136,7 @@ JSON.isValid = isValidJSON;
|
|||
JSON.safeParse = safeParseJSON;
|
||||
|
||||
import { byId } from "./shorthands";
|
||||
|
||||
window.byId = byId;
|
||||
Node.prototype.on = function (name, fn, options) {
|
||||
this.addEventListener(name, fn, options);
|
||||
|
|
@ -87,27 +148,63 @@ Node.prototype.off = function (name, fn) {
|
|||
};
|
||||
|
||||
declare global {
|
||||
|
||||
interface JSON {
|
||||
isValid: (str: string) => boolean;
|
||||
safeParse: (str: string) => any;
|
||||
}
|
||||
|
||||
interface Node {
|
||||
on: (name: string, fn: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => Node;
|
||||
on: (
|
||||
name: string,
|
||||
fn: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
) => Node;
|
||||
off: (name: string, fn: EventListenerOrEventListenerObject) => Node;
|
||||
}
|
||||
}
|
||||
|
||||
import { shouldRegenerateGrid, generateGrid, findGridAll, findGridCell, findClosestCell, calculateVoronoi, findAllCellsInRadius, getPackPolygon, getGridPolygon, poissonDiscSampler, isLand, isWater, findAllInQuadtree, drawHeights } from "./graphUtils";
|
||||
window.shouldRegenerateGrid = (grid: any, expectedSeed: number) => shouldRegenerateGrid(grid, expectedSeed, (window as any).graphWidth, (window as any).graphHeight);
|
||||
window.generateGrid = () => generateGrid((window as any).seed, (window as any).graphWidth, (window as any).graphHeight);
|
||||
window.findGridAll = (x: number, y: number, radius: number) => findGridAll(x, y, radius, (window as any).grid);
|
||||
window.findGridCell = (x: number, y: number) => findGridCell(x, y, (window as any).grid);
|
||||
window.findCell = (x: number, y: number, radius?: number) => findClosestCell(x, y, radius, (window as any).pack);
|
||||
window.findAll = (x: number, y: number, radius: number) => findAllCellsInRadius(x, y, radius, (window as any).pack);
|
||||
window.getPackPolygon = (cellIndex: number) => getPackPolygon(cellIndex, (window as any).pack);
|
||||
window.getGridPolygon = (cellIndex: number) => getGridPolygon(cellIndex, (window as any).grid);
|
||||
import {
|
||||
calculateVoronoi,
|
||||
drawHeights,
|
||||
findAllCellsInRadius,
|
||||
findAllInQuadtree,
|
||||
findClosestCell,
|
||||
findGridAll,
|
||||
findGridCell,
|
||||
generateGrid,
|
||||
getGridPolygon,
|
||||
getPackPolygon,
|
||||
isLand,
|
||||
isWater,
|
||||
poissonDiscSampler,
|
||||
shouldRegenerateGrid,
|
||||
} from "./graphUtils";
|
||||
|
||||
window.shouldRegenerateGrid = (grid: any, expectedSeed: number) =>
|
||||
shouldRegenerateGrid(
|
||||
grid,
|
||||
expectedSeed,
|
||||
(window as any).graphWidth,
|
||||
(window as any).graphHeight,
|
||||
);
|
||||
window.generateGrid = () =>
|
||||
generateGrid(
|
||||
(window as any).seed,
|
||||
(window as any).graphWidth,
|
||||
(window as any).graphHeight,
|
||||
);
|
||||
window.findGridAll = (x: number, y: number, radius: number) =>
|
||||
findGridAll(x, y, radius, (window as any).grid);
|
||||
window.findGridCell = (x: number, y: number) =>
|
||||
findGridCell(x, y, (window as any).grid);
|
||||
window.findCell = (x: number, y: number, radius?: number) =>
|
||||
findClosestCell(x, y, radius, (window as any).pack);
|
||||
window.findAll = (x: number, y: number, radius: number) =>
|
||||
findAllCellsInRadius(x, y, radius, (window as any).pack);
|
||||
window.getPackPolygon = (cellIndex: number) =>
|
||||
getPackPolygon(cellIndex, (window as any).pack);
|
||||
window.getGridPolygon = (cellIndex: number) =>
|
||||
getGridPolygon(cellIndex, (window as any).grid);
|
||||
window.calculateVoronoi = calculateVoronoi;
|
||||
window.poissonDiscSampler = poissonDiscSampler;
|
||||
window.findAllInQuadtree = findAllInQuadtree;
|
||||
|
|
@ -115,8 +212,26 @@ window.drawHeights = drawHeights;
|
|||
window.isLand = (i: number) => isLand(i, (window as any).pack);
|
||||
window.isWater = (i: number) => isWater(i, (window as any).pack);
|
||||
|
||||
import { clipPoly, getSegmentId, debounce, throttle, parseError, getBase64, openURL, wiki, link, isCtrlClick, generateDate, getLongitude, getLatitude, getCoordinates, initializePrompt } from "./commonUtils";
|
||||
window.clipPoly = (points: [number, number][], secure?: number) => clipPoly(points, graphWidth, graphHeight, secure);
|
||||
import {
|
||||
clipPoly,
|
||||
debounce,
|
||||
generateDate,
|
||||
getBase64,
|
||||
getCoordinates,
|
||||
getLatitude,
|
||||
getLongitude,
|
||||
getSegmentId,
|
||||
initializePrompt,
|
||||
isCtrlClick,
|
||||
link,
|
||||
openURL,
|
||||
parseError,
|
||||
throttle,
|
||||
wiki,
|
||||
} from "./commonUtils";
|
||||
|
||||
window.clipPoly = (points: [number, number][], secure?: number) =>
|
||||
clipPoly(points, graphWidth, graphHeight, secure);
|
||||
window.getSegmentId = getSegmentId;
|
||||
window.debounce = debounce;
|
||||
window.throttle = throttle;
|
||||
|
|
@ -127,25 +242,37 @@ window.wiki = wiki;
|
|||
window.link = link;
|
||||
window.isCtrlClick = isCtrlClick;
|
||||
window.generateDate = generateDate;
|
||||
window.getLongitude = (x: number, decimals?: number) => getLongitude(x, mapCoordinates, graphWidth, decimals);
|
||||
window.getLatitude = (y: number, decimals?: number) => getLatitude(y, mapCoordinates, graphHeight, decimals);
|
||||
window.getCoordinates = (x: number, y: number, decimals?: number) => getCoordinates(x, y, mapCoordinates, graphWidth, graphHeight, decimals);
|
||||
window.getLongitude = (x: number, decimals?: number) =>
|
||||
getLongitude(x, mapCoordinates, graphWidth, decimals);
|
||||
window.getLatitude = (y: number, decimals?: number) =>
|
||||
getLatitude(y, mapCoordinates, graphHeight, decimals);
|
||||
window.getCoordinates = (x: number, y: number, decimals?: number) =>
|
||||
getCoordinates(x, y, mapCoordinates, graphWidth, graphHeight, decimals);
|
||||
|
||||
// Initialize prompt when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePrompt);
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializePrompt);
|
||||
} else {
|
||||
initializePrompt();
|
||||
}
|
||||
|
||||
import { drawCellsValue, drawPolygons, drawRouteConnections, drawPoint, drawPath } from "./debugUtils";
|
||||
window.drawCellsValue = (data:any[]) => drawCellsValue(data, (window as any).pack);
|
||||
window.drawPolygons = (data: any[]) => drawPolygons(data, (window as any).terrs, (window as any).grid);
|
||||
window.drawRouteConnections = () => drawRouteConnections((window as any).packedGraph);
|
||||
import {
|
||||
drawCellsValue,
|
||||
drawPath,
|
||||
drawPoint,
|
||||
drawPolygons,
|
||||
drawRouteConnections,
|
||||
} from "./debugUtils";
|
||||
|
||||
window.drawCellsValue = (data: any[]) =>
|
||||
drawCellsValue(data, (window as any).pack);
|
||||
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;
|
||||
|
||||
|
||||
export {
|
||||
rn,
|
||||
lim,
|
||||
|
|
@ -232,5 +359,5 @@ export {
|
|||
drawPolygons,
|
||||
drawRouteConnections,
|
||||
drawPoint,
|
||||
drawPath
|
||||
}
|
||||
drawPath,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { P } from "./probabilityUtils";
|
|||
export const isVowel = (c: string): boolean => {
|
||||
const VOWELS = `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`;
|
||||
return VOWELS.includes(c);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove trailing vowels from a string until it reaches a minimum length.
|
||||
|
|
@ -22,8 +22,7 @@ export const trimVowels = (string: string, minLength: number = 3) => {
|
|||
string = string.slice(0, -1);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Get adjective form of a noun based on predefined rules.
|
||||
|
|
@ -35,131 +34,133 @@ export const getAdjective = (nounToBeAdjective: string) => {
|
|||
{
|
||||
name: "guo",
|
||||
probability: 1,
|
||||
condition: new RegExp(" Guo$"),
|
||||
action: (noun: string) => noun.slice(0, -4)
|
||||
condition: / Guo$/,
|
||||
action: (noun: string) => noun.slice(0, -4),
|
||||
},
|
||||
{
|
||||
name: "orszag",
|
||||
probability: 1,
|
||||
condition: new RegExp("orszag$"),
|
||||
action: (noun: string) => (noun.length < 9 ? noun + "ian" : noun.slice(0, -6))
|
||||
condition: /orszag$/,
|
||||
action: (noun: string) =>
|
||||
noun.length < 9 ? `${noun}ian` : noun.slice(0, -6),
|
||||
},
|
||||
{
|
||||
name: "stan",
|
||||
probability: 1,
|
||||
condition: new RegExp("stan$"),
|
||||
action: (noun: string) => (noun.length < 9 ? noun + "i" : trimVowels(noun.slice(0, -4)))
|
||||
condition: /stan$/,
|
||||
action: (noun: string) =>
|
||||
noun.length < 9 ? `${noun}i` : trimVowels(noun.slice(0, -4)),
|
||||
},
|
||||
{
|
||||
name: "land",
|
||||
probability: 1,
|
||||
condition: new RegExp("land$"),
|
||||
condition: /land$/,
|
||||
action: (noun: string) => {
|
||||
if (noun.length > 9) return noun.slice(0, -4);
|
||||
const root = trimVowels(noun.slice(0, -4), 0);
|
||||
if (root.length < 3) return noun + "ic";
|
||||
if (root.length < 4) return root + "lish";
|
||||
return root + "ish";
|
||||
}
|
||||
if (root.length < 3) return `${noun}ic`;
|
||||
if (root.length < 4) return `${root}lish`;
|
||||
return `${root}ish`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "que",
|
||||
probability: 1,
|
||||
condition: new RegExp("que$"),
|
||||
action: (noun: string) => noun.replace(/que$/, "can")
|
||||
condition: /que$/,
|
||||
action: (noun: string) => noun.replace(/que$/, "can"),
|
||||
},
|
||||
{
|
||||
name: "a",
|
||||
probability: 1,
|
||||
condition: new RegExp("a$"),
|
||||
action: (noun: string) => noun + "n"
|
||||
condition: /a$/,
|
||||
action: (noun: string) => `${noun}n`,
|
||||
},
|
||||
{
|
||||
name: "o",
|
||||
probability: 1,
|
||||
condition: new RegExp("o$"),
|
||||
action: (noun: string) => noun.replace(/o$/, "an")
|
||||
condition: /o$/,
|
||||
action: (noun: string) => noun.replace(/o$/, "an"),
|
||||
},
|
||||
{
|
||||
name: "u",
|
||||
probability: 1,
|
||||
condition: new RegExp("u$"),
|
||||
action: (noun: string) => noun + "an"
|
||||
condition: /u$/,
|
||||
action: (noun: string) => `${noun}an`,
|
||||
},
|
||||
{
|
||||
name: "i",
|
||||
probability: 1,
|
||||
condition: new RegExp("i$"),
|
||||
action: (noun: string) => noun + "an"
|
||||
condition: /i$/,
|
||||
action: (noun: string) => `${noun}an`,
|
||||
},
|
||||
{
|
||||
name: "e",
|
||||
probability: 1,
|
||||
condition: new RegExp("e$"),
|
||||
action: (noun: string) => noun + "an"
|
||||
condition: /e$/,
|
||||
action: (noun: string) => `${noun}an`,
|
||||
},
|
||||
{
|
||||
name: "ay",
|
||||
probability: 1,
|
||||
condition: new RegExp("ay$"),
|
||||
action: (noun: string) => noun + "an"
|
||||
condition: /ay$/,
|
||||
action: (noun: string) => `${noun}an`,
|
||||
},
|
||||
{
|
||||
name: "os",
|
||||
probability: 1,
|
||||
condition: new RegExp("os$"),
|
||||
condition: /os$/,
|
||||
action: (noun: string) => {
|
||||
const root = trimVowels(noun.slice(0, -2), 0);
|
||||
if (root.length < 4) return noun.slice(0, -1);
|
||||
return root + "ian";
|
||||
}
|
||||
return `${root}ian`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "es",
|
||||
probability: 1,
|
||||
condition: new RegExp("es$"),
|
||||
condition: /es$/,
|
||||
action: (noun: string) => {
|
||||
const root = trimVowels(noun.slice(0, -2), 0);
|
||||
if (root.length > 7) return noun.slice(0, -1);
|
||||
return root + "ian";
|
||||
}
|
||||
return `${root}ian`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "l",
|
||||
probability: 0.8,
|
||||
condition: new RegExp("l$"),
|
||||
action: (noun: string) => noun + "ese"
|
||||
condition: /l$/,
|
||||
action: (noun: string) => `${noun}ese`,
|
||||
},
|
||||
{
|
||||
name: "n",
|
||||
probability: 0.8,
|
||||
condition: new RegExp("n$"),
|
||||
action: (noun: string) => noun + "ese"
|
||||
condition: /n$/,
|
||||
action: (noun: string) => `${noun}ese`,
|
||||
},
|
||||
{
|
||||
name: "ad",
|
||||
probability: 0.8,
|
||||
condition: new RegExp("ad$"),
|
||||
action: (noun: string) => noun + "ian"
|
||||
condition: /ad$/,
|
||||
action: (noun: string) => `${noun}ian`,
|
||||
},
|
||||
{
|
||||
name: "an",
|
||||
probability: 0.8,
|
||||
condition: new RegExp("an$"),
|
||||
action: (noun: string) => noun + "ian"
|
||||
condition: /an$/,
|
||||
action: (noun: string) => `${noun}ian`,
|
||||
},
|
||||
{
|
||||
name: "ish",
|
||||
probability: 0.25,
|
||||
condition: new RegExp("^[a-zA-Z]{6}$"),
|
||||
action: (noun: string) => trimVowels(noun.slice(0, -1)) + "ish"
|
||||
condition: /^[a-zA-Z]{6}$/,
|
||||
action: (noun: string) => `${trimVowels(noun.slice(0, -1))}ish`,
|
||||
},
|
||||
{
|
||||
name: "an",
|
||||
probability: 0.5,
|
||||
condition: new RegExp("^[a-zA-Z]{0,7}$"),
|
||||
action: (noun: string) => trimVowels(noun) + "an"
|
||||
}
|
||||
condition: /^[a-zA-Z]{0,7}$/,
|
||||
action: (noun: string) => `${trimVowels(noun)}an`,
|
||||
},
|
||||
];
|
||||
for (const rule of adjectivizationRules) {
|
||||
if (P(rule.probability) && rule.condition.test(nounToBeAdjective)) {
|
||||
|
|
@ -167,14 +168,15 @@ export const getAdjective = (nounToBeAdjective: string) => {
|
|||
}
|
||||
}
|
||||
return nounToBeAdjective; // no rule applied, return noun as is
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the ordinal suffix for a given number.
|
||||
* @param n - The number.
|
||||
* @returns The number with its ordinal suffix.
|
||||
*/
|
||||
export const nth = (n: number) => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
|
||||
export const nth = (n: number) =>
|
||||
n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
|
||||
|
||||
/**
|
||||
* Generate an abbreviation for a given name, avoiding restricted codes.
|
||||
|
|
@ -187,12 +189,13 @@ export const abbreviate = (name: string, restricted: string[] = []) => {
|
|||
const words = parsed.split(" ");
|
||||
const letters = words.join("");
|
||||
|
||||
let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
|
||||
let code =
|
||||
words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
|
||||
for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
|
||||
code = letters[0] + letters[i].toUpperCase();
|
||||
}
|
||||
return code;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a list of strings into a human-readable list.
|
||||
|
|
@ -201,9 +204,12 @@ export const abbreviate = (name: string, restricted: string[] = []) => {
|
|||
*/
|
||||
export const list = (array: string[]) => {
|
||||
if (!Intl.ListFormat) return array.join(", ");
|
||||
const conjunction = new Intl.ListFormat(document.documentElement.lang || "en", {style: "long", type: "conjunction"});
|
||||
const conjunction = new Intl.ListFormat(
|
||||
document.documentElement.lang || "en",
|
||||
{ style: "long", type: "conjunction" },
|
||||
);
|
||||
return conjunction.format(array);
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -214,4 +220,4 @@ declare global {
|
|||
abbreviate: typeof abbreviate;
|
||||
list: typeof list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
* @param {Node | Window} node - The starting node or window
|
||||
* @returns {Array<Node>} - The composed path as an array
|
||||
*/
|
||||
export const getComposedPath = function(node: any): Array<Node | Window> {
|
||||
let parent;
|
||||
export const getComposedPath = (node: any): Array<Node | Window> => {
|
||||
let parent: Node | Window | undefined;
|
||||
if (node.parentNode) parent = node.parentNode;
|
||||
else if (node.host) parent = node.host;
|
||||
else if (node.defaultView) parent = node.defaultView;
|
||||
if (parent !== undefined) return [node].concat(getComposedPath(parent));
|
||||
return [node];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a unique ID for a given core string
|
||||
|
|
@ -18,14 +18,14 @@ export const getComposedPath = function(node: any): Array<Node | Window> {
|
|||
* @param {number} [i=1] - The starting index
|
||||
* @returns {string} - The unique ID
|
||||
*/
|
||||
export const getNextId = function(core: string, i: number = 1): string {
|
||||
export const getNextId = (core: string, i: number = 1): string => {
|
||||
while (document.getElementById(core + i)) i++;
|
||||
return core + i;
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
getComposedPath: typeof getComposedPath;
|
||||
getNextId: typeof getNextId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
* @returns The rounded number.
|
||||
*/
|
||||
export const rn = (v: number, d: number = 0) => {
|
||||
const m = Math.pow(10, d);
|
||||
const m = 10 ** d;
|
||||
return Math.round(v * m) / m;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamps a number between a minimum and maximum value.
|
||||
|
|
@ -18,7 +18,7 @@ export const rn = (v: number, d: number = 0) => {
|
|||
*/
|
||||
export const minmax = (value: number, min: number, max: number) => {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamps a number between 0 and 100.
|
||||
|
|
@ -27,7 +27,7 @@ export const minmax = (value: number, min: number, max: number) => {
|
|||
*/
|
||||
export const lim = (v: number) => {
|
||||
return minmax(v, 0, 100);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes a number within a specified range to a value between 0 and 1.
|
||||
|
|
@ -38,7 +38,7 @@ export const lim = (v: number) => {
|
|||
*/
|
||||
export const normalize = (val: number, min: number, max: number) => {
|
||||
return minmax((val - min) / (max - min), 0, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs linear interpolation between two values.
|
||||
|
|
@ -49,7 +49,7 @@ export const normalize = (val: number, min: number, max: number) => {
|
|||
*/
|
||||
export const lerp = (a: number, b: number, t: number) => {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -59,4 +59,4 @@ declare global {
|
|||
normalize: typeof normalize;
|
||||
lerp: typeof lerp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import { rn } from "./numberUtils";
|
|||
* @returns {string} SVG path data for the filled shape.
|
||||
*/
|
||||
const getFillPath = (vertices: any, vertexChain: number[]) => {
|
||||
const points = vertexChain.map(vertexId => vertices.p[vertexId]);
|
||||
const points = vertexChain.map((vertexId) => vertices.p[vertexId]);
|
||||
const firstPoint = points.shift();
|
||||
return `M${firstPoint} L${points.join(" ")} Z`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates SVG path data for borders based on a chain of vertices and a discontinuation condition.
|
||||
|
|
@ -20,10 +20,14 @@ const getFillPath = (vertices: any, vertexChain: number[]) => {
|
|||
* @param {(vertexId: number) => boolean} discontinue - A function that determines if the path should discontinue at a vertex.
|
||||
* @returns {string} SVG path data for the border.
|
||||
*/
|
||||
const getBorderPath = (vertices: any, vertexChain: number[], discontinue: (vertexId: number) => boolean) => {
|
||||
const getBorderPath = (
|
||||
vertices: any,
|
||||
vertexChain: number[],
|
||||
discontinue: (vertexId: number) => boolean,
|
||||
) => {
|
||||
let discontinued = true;
|
||||
let lastOperation = "";
|
||||
const path = vertexChain.map(vertexId => {
|
||||
const path = vertexChain.map((vertexId) => {
|
||||
if (discontinue(vertexId)) {
|
||||
discontinued = true;
|
||||
return "";
|
||||
|
|
@ -33,12 +37,13 @@ const getBorderPath = (vertices: any, vertexChain: number[], discontinue: (verte
|
|||
discontinued = false;
|
||||
lastOperation = operation;
|
||||
|
||||
const command = operation === "L" && operation === lastOperation ? "" : operation;
|
||||
const command =
|
||||
operation === "L" && operation === lastOperation ? "" : operation;
|
||||
return ` ${command}${vertices.p[vertexId]}`;
|
||||
});
|
||||
|
||||
return path.join("").trim();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores the path from exit to start using the 'from' mapping.
|
||||
|
|
@ -62,7 +67,7 @@ const restorePath = (exit: number, start: number, from: number[]) => {
|
|||
pathCells.push(current);
|
||||
|
||||
return pathCells.reverse();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns isolines (borders) for different types of cells in the graph.
|
||||
|
|
@ -75,12 +80,23 @@ const restorePath = (exit: number, start: number, from: number[]) => {
|
|||
* @param {boolean} [options.waterGap=false] - Whether to generate water gap paths for each type.
|
||||
* @returns {object} An object containing isolines for each type based on the specified options.
|
||||
*/
|
||||
export const getIsolines = (graph: any, getType: (cellId: number) => any, options: {polygons?: boolean, fill?: boolean, halo?: boolean, waterGap?: boolean} = {polygons: false, fill: false, halo: false, waterGap: false}): any => {
|
||||
const {cells, vertices} = graph;
|
||||
export const getIsolines = (
|
||||
graph: any,
|
||||
getType: (cellId: number) => any,
|
||||
options: {
|
||||
polygons?: boolean;
|
||||
fill?: boolean;
|
||||
halo?: boolean;
|
||||
waterGap?: boolean;
|
||||
} = { polygons: false, fill: false, halo: false, waterGap: false },
|
||||
): any => {
|
||||
const { cells, vertices } = graph;
|
||||
const isolines: any = {};
|
||||
|
||||
const checkedCells = new Uint8Array(cells.i.length);
|
||||
const addToChecked = (cellId: number) => (checkedCells[cellId] = 1);
|
||||
const addToChecked = (cellId: number) => {
|
||||
checkedCells[cellId] = 1;
|
||||
};
|
||||
const isChecked = (cellId: number) => checkedCells[cellId] === 1;
|
||||
|
||||
for (const cellId of cells.i) {
|
||||
|
|
@ -96,12 +112,22 @@ export const getIsolines = (graph: any, getType: (cellId: number) => any, option
|
|||
|
||||
// check if inner lake. Note there is no shoreline for grid features
|
||||
const feature = graph.features[cells.f[onborderCell]];
|
||||
if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
|
||||
if (feature.type === "lake" && feature.shoreline?.every(ofSameType))
|
||||
continue;
|
||||
|
||||
const startingVertex = cells.v[cellId].find((v: number) => vertices.c[v].some(ofDifferentType));
|
||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
const startingVertex = cells.v[cellId].find((v: number) =>
|
||||
vertices.c[v].some(ofDifferentType),
|
||||
);
|
||||
if (startingVertex === undefined)
|
||||
throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
|
||||
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
const vertexChain = connectVertices({
|
||||
vertices,
|
||||
startingVertex,
|
||||
ofSameType,
|
||||
addToChecked,
|
||||
closeRing: true,
|
||||
});
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
addIsolineTo(type, vertices, vertexChain, isolines, options);
|
||||
|
|
@ -109,12 +135,20 @@ export const getIsolines = (graph: any, getType: (cellId: number) => any, option
|
|||
|
||||
return isolines;
|
||||
|
||||
function addIsolineTo(type: any, vertices: any, vertexChain: number[], isolines: any, options: any) {
|
||||
function addIsolineTo(
|
||||
type: any,
|
||||
vertices: any,
|
||||
vertexChain: number[],
|
||||
isolines: any,
|
||||
options: any,
|
||||
) {
|
||||
if (!isolines[type]) isolines[type] = {};
|
||||
|
||||
if (options.polygons) {
|
||||
if (!isolines[type].polygons) isolines[type].polygons = [];
|
||||
isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
|
||||
isolines[type].polygons.push(
|
||||
vertexChain.map((vertexId) => vertices.p[vertexId]),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.fill) {
|
||||
|
|
@ -124,18 +158,27 @@ export const getIsolines = (graph: any, getType: (cellId: number) => any, option
|
|||
|
||||
if (options.waterGap) {
|
||||
if (!isolines[type].waterGap) isolines[type].waterGap = "";
|
||||
const isLandVertex = (vertexId: number) => vertices.c[vertexId].every((i: number) => cells.h[i] >= 20);
|
||||
isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
|
||||
const isLandVertex = (vertexId: number) =>
|
||||
vertices.c[vertexId].every((i: number) => cells.h[i] >= 20);
|
||||
isolines[type].waterGap += getBorderPath(
|
||||
vertices,
|
||||
vertexChain,
|
||||
isLandVertex,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.halo) {
|
||||
if (!isolines[type].halo) isolines[type].halo = "";
|
||||
const isBorderVertex = (vertexId: number) => vertices.c[vertexId].some((i: number) => cells.b[i]);
|
||||
isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
|
||||
const isBorderVertex = (vertexId: number) =>
|
||||
vertices.c[vertexId].some((i: number) => cells.b[i]);
|
||||
isolines[type].halo += getBorderPath(
|
||||
vertices,
|
||||
vertexChain,
|
||||
isBorderVertex,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates SVG path data for the border of a shape defined by a chain of vertices.
|
||||
|
|
@ -144,14 +187,18 @@ export const getIsolines = (graph: any, getType: (cellId: number) => any, option
|
|||
* @returns {string} SVG path data for the border of the shape.
|
||||
*/
|
||||
export const getVertexPath = (cellsArray: number[], packedGraph: any = {}) => {
|
||||
const {cells, vertices} = packedGraph;
|
||||
const { cells, vertices } = packedGraph;
|
||||
|
||||
const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
|
||||
const cellsObj = Object.fromEntries(
|
||||
cellsArray.map((cellId) => [cellId, true]),
|
||||
);
|
||||
const ofSameType = (cellId: number) => cellsObj[cellId];
|
||||
const ofDifferentType = (cellId: number) => !cellsObj[cellId];
|
||||
|
||||
const checkedCells = new Uint8Array(cells.c.length);
|
||||
const addToChecked = (cellId: number) => (checkedCells[cellId] = 1);
|
||||
const addToChecked = (cellId: number) => {
|
||||
checkedCells[cellId] = 1;
|
||||
};
|
||||
const isChecked = (cellId: number) => checkedCells[cellId] === 1;
|
||||
let path = "";
|
||||
|
||||
|
|
@ -166,17 +213,26 @@ export const getVertexPath = (cellsArray: number[], packedGraph: any = {}) => {
|
|||
if (feature.shoreline.every(ofSameType)) continue; // inner lake
|
||||
}
|
||||
|
||||
const startingVertex = cells.v[cellId].find((v: number) => vertices.c[v].some(ofDifferentType));
|
||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
const startingVertex = cells.v[cellId].find((v: number) =>
|
||||
vertices.c[v].some(ofDifferentType),
|
||||
);
|
||||
if (startingVertex === undefined)
|
||||
throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
|
||||
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
const vertexChain = connectVertices({
|
||||
vertices,
|
||||
startingVertex,
|
||||
ofSameType,
|
||||
addToChecked,
|
||||
closeRing: true,
|
||||
});
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
path += getFillPath(vertices, vertexChain);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the poles of inaccessibility for each type of cell in the graph.
|
||||
|
|
@ -184,17 +240,22 @@ export const getVertexPath = (cellsArray: number[], packedGraph: any = {}) => {
|
|||
* @param {(cellId: number) => any} getType - A function that returns the type of a cell given its ID.
|
||||
* @returns {object} An object mapping each type to its pole of inaccessibility coordinates [x, y].
|
||||
*/
|
||||
export const getPolesOfInaccessibility = (graph: any, getType: (cellId: number) => any) => {
|
||||
const isolines = getIsolines(graph, getType, {polygons: true});
|
||||
export const getPolesOfInaccessibility = (
|
||||
graph: any,
|
||||
getType: (cellId: number) => any,
|
||||
) => {
|
||||
const isolines = getIsolines(graph, getType, { polygons: true });
|
||||
|
||||
const poles = Object.entries(isolines).map(([id, isoline]) => {
|
||||
const multiPolygon = (isoline as any).polygons.sort((a: any, b: any) => b.length - a.length);
|
||||
const multiPolygon = (isoline as any).polygons.sort(
|
||||
(a: any, b: any) => b.length - a.length,
|
||||
);
|
||||
const [x, y] = polylabel(multiPolygon, 20);
|
||||
return [id, [rn(x), rn(y)]];
|
||||
});
|
||||
|
||||
return Object.fromEntries(poles);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects vertices to form a closed path based on cell type.
|
||||
|
|
@ -206,7 +267,19 @@ export const getPolesOfInaccessibility = (graph: any, getType: (cellId: number)
|
|||
* @param {boolean} [options.closeRing=false] - Whether to close the path into a ring.
|
||||
* @returns {number[]} An array of vertex IDs forming the connected path.
|
||||
*/
|
||||
export const connectVertices = ({vertices, startingVertex, ofSameType, addToChecked, closeRing}: {vertices: any, startingVertex: number, ofSameType: (cellId: number) => boolean, addToChecked?: (cellId: number) => void, closeRing?: boolean}) => {
|
||||
export const connectVertices = ({
|
||||
vertices,
|
||||
startingVertex,
|
||||
ofSameType,
|
||||
addToChecked,
|
||||
closeRing,
|
||||
}: {
|
||||
vertices: any;
|
||||
startingVertex: number;
|
||||
ofSameType: (cellId: number) => boolean;
|
||||
addToChecked?: (cellId: number) => void;
|
||||
closeRing?: boolean;
|
||||
}) => {
|
||||
const MAX_ITERATIONS = vertices.c.length;
|
||||
const chain = []; // vertices chain to form a path
|
||||
|
||||
|
|
@ -227,24 +300,30 @@ export const connectVertices = ({vertices, startingVertex, ofSameType, addToChec
|
|||
else if (v3 !== previous && c1 !== c3) next = v3;
|
||||
|
||||
if (next >= vertices.c.length) {
|
||||
window.ERROR && console.error("ConnectVertices: next vertex is out of bounds");
|
||||
window.ERROR &&
|
||||
console.error("ConnectVertices: next vertex is out of bounds");
|
||||
break;
|
||||
}
|
||||
|
||||
if (next === current) {
|
||||
window.ERROR && console.error("ConnectVertices: next vertex is not found");
|
||||
window.ERROR &&
|
||||
console.error("ConnectVertices: next vertex is not found");
|
||||
break;
|
||||
}
|
||||
|
||||
if (i === MAX_ITERATIONS) {
|
||||
window.ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS);
|
||||
window.ERROR &&
|
||||
console.error(
|
||||
"ConnectVertices: max iterations reached",
|
||||
MAX_ITERATIONS,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (closeRing) chain.push(startingVertex);
|
||||
return chain;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the shortest path between two cells using a cost-based pathfinding algorithm.
|
||||
|
|
@ -254,7 +333,12 @@ export const connectVertices = ({vertices, startingVertex, ofSameType, addToChec
|
|||
* @param {object} packedGraph - The packed graph object containing cells and their connections.
|
||||
* @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same.
|
||||
*/
|
||||
export const findPath = (start: number, isExit: (id: number) => boolean, getCost: (current: number, next: number) => number, packedGraph: any = {}): number[] | null => {
|
||||
export const findPath = (
|
||||
start: number,
|
||||
isExit: (id: number) => boolean,
|
||||
getCost: (current: number, next: number) => number,
|
||||
packedGraph: any = {},
|
||||
): number[] | null => {
|
||||
if (isExit(start)) return null;
|
||||
|
||||
const from = [];
|
||||
|
|
@ -284,7 +368,7 @@ export const findPath = (start: number, isExit: (id: number) => boolean, getCost
|
|||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -297,4 +381,4 @@ declare global {
|
|||
findPath: typeof findPath;
|
||||
getVertexPath: typeof getVertexPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// replaceAll
|
||||
if (String.prototype.replaceAll === undefined) {
|
||||
String.prototype.replaceAll = function (str: string | RegExp, newStr: string | ((substring: string, ...args: any[]) => string)): string {
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str as RegExp, newStr as any);
|
||||
String.prototype.replaceAll = function (
|
||||
str: string | RegExp,
|
||||
newStr: string | ((substring: string, ...args: any[]) => string),
|
||||
): string {
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]")
|
||||
return this.replace(str as RegExp, newStr as any);
|
||||
return this.replace(new RegExp(str, "g"), newStr as any);
|
||||
};
|
||||
}
|
||||
|
|
@ -9,7 +13,13 @@ if (String.prototype.replaceAll === undefined) {
|
|||
// flat
|
||||
if (Array.prototype.flat === undefined) {
|
||||
Array.prototype.flat = function <T>(this: T[], depth?: number): any[] {
|
||||
return (this as Array<unknown>).reduce((acc: any[], val: unknown) => (Array.isArray(val) ? acc.concat((val as any).flat(depth)) : acc.concat(val)), []);
|
||||
return (this as Array<unknown>).reduce(
|
||||
(acc: any[], val: unknown) =>
|
||||
Array.isArray(val)
|
||||
? acc.concat((val as any).flat(depth))
|
||||
: acc.concat(val),
|
||||
[],
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -24,11 +34,13 @@ if (Array.prototype.at === undefined) {
|
|||
|
||||
// readable stream iterator: https://bugs.chromium.org/p/chromium/issues/detail?id=929585#c10
|
||||
if ((ReadableStream.prototype as any)[Symbol.asyncIterator] === undefined) {
|
||||
(ReadableStream.prototype as any)[Symbol.asyncIterator] = async function* <R>(this: ReadableStream<R>): AsyncGenerator<R, void, unknown> {
|
||||
(ReadableStream.prototype as any)[Symbol.asyncIterator] = async function* <R>(
|
||||
this: ReadableStream<R>,
|
||||
): AsyncGenerator<R, void, unknown> {
|
||||
const reader = this.getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
const { done, value } = await reader.read();
|
||||
if (done) return;
|
||||
yield value;
|
||||
}
|
||||
|
|
@ -40,7 +52,10 @@ if ((ReadableStream.prototype as any)[Symbol.asyncIterator] === undefined) {
|
|||
|
||||
declare global {
|
||||
interface String {
|
||||
replaceAll(searchValue: string | RegExp, replaceValue: string | ((substring: string, ...args: any[]) => string)): string;
|
||||
replaceAll(
|
||||
searchValue: string | RegExp,
|
||||
replaceValue: string | ((substring: string, ...args: any[]) => string),
|
||||
): string;
|
||||
}
|
||||
|
||||
interface Array<T> {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { minmax, rn } from "./numberUtils";
|
||||
import { randomNormal } from "d3";
|
||||
import { minmax, rn } from "./numberUtils";
|
||||
|
||||
/**
|
||||
* Creates a random number between min and max (inclusive).
|
||||
|
|
@ -14,7 +14,7 @@ export const rand = (min: number, max?: number): number => {
|
|||
min = 0;
|
||||
}
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a boolean based on the given probability.
|
||||
|
|
@ -25,7 +25,7 @@ export const P = (probability: number): boolean => {
|
|||
if (probability >= 1) return true;
|
||||
if (probability <= 0) return false;
|
||||
return Math.random() < probability;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true every n times.
|
||||
|
|
@ -34,7 +34,7 @@ export const P = (probability: number): boolean => {
|
|||
*/
|
||||
export const each = (n: number) => {
|
||||
return (i: number) => i % n === 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Random Gaussian number generator
|
||||
|
|
@ -46,10 +46,23 @@ export const each = (n: number) => {
|
|||
* @param {number} round - round value to n decimals
|
||||
* @return {number} random number
|
||||
*/
|
||||
export const gauss = (expected = 100, deviation = 30, min = 0, max = 300, round = 0) => {
|
||||
export const gauss = (
|
||||
expected = 100,
|
||||
deviation = 30,
|
||||
min = 0,
|
||||
max = 300,
|
||||
round = 0,
|
||||
) => {
|
||||
// Use .source() to get a version that uses the current Math.random (which may be seeded)
|
||||
return rn(minmax(randomNormal.source(() => Math.random())(expected, deviation)(), min, max), round);
|
||||
}
|
||||
return rn(
|
||||
minmax(
|
||||
randomNormal.source(() => Math.random())(expected, deviation)(),
|
||||
min,
|
||||
max,
|
||||
),
|
||||
round,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the integer part of a float plus one with the probability of the decimal part.
|
||||
|
|
@ -58,7 +71,7 @@ export const gauss = (expected = 100, deviation = 30, min = 0, max = 300, round
|
|||
*/
|
||||
export const Pint = (float: number): number => {
|
||||
return ~~float + +P(float % 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random element from an array.
|
||||
|
|
@ -67,18 +80,18 @@ export const Pint = (float: number): number => {
|
|||
*/
|
||||
export const ra = (array: any[]): any => {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random key from an object where values are weights.
|
||||
* @param {Object} object - object with keys and their weights
|
||||
* @return {string} a random key based on weights
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const obj = { a: 1, b: 3, c: 6 };
|
||||
* const randomKey = rw(obj); // 'a' has 10% chance, 'b' has 30% chance, 'c' has 60% chance
|
||||
*/
|
||||
export const rw = (object: {[key: string]: number}): string => {
|
||||
export const rw = (object: { [key: string]: number }): string => {
|
||||
const array = [];
|
||||
for (const key in object) {
|
||||
for (let i = 0; i < object[key]; i++) {
|
||||
|
|
@ -86,7 +99,7 @@ export const rw = (object: {[key: string]: number}): string => {
|
|||
}
|
||||
}
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min).
|
||||
|
|
@ -96,8 +109,8 @@ export const rw = (object: {[key: string]: number}): string => {
|
|||
* @return {number} biased random integer
|
||||
*/
|
||||
export const biased = (min: number, max: number, ex: number): number => {
|
||||
return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
|
||||
}
|
||||
return Math.round(min + (max - min) * Math.random() ** ex);
|
||||
};
|
||||
|
||||
const ERROR = false;
|
||||
/**
|
||||
|
|
@ -110,28 +123,28 @@ export const getNumberInRange = (r: string): number => {
|
|||
ERROR && console.error("Range value should be a string", r);
|
||||
return 0;
|
||||
}
|
||||
if (!isNaN(+r)) return ~~r + +P(+r - ~~r);
|
||||
if (!Number.isNaN(+r)) return ~~r + +P(+r - ~~r);
|
||||
const sign = r[0] === "-" ? -1 : 1;
|
||||
if (isNaN(+r[0])) r = r.slice(1);
|
||||
if (Number.isNaN(+r[0])) r = r.slice(1);
|
||||
const range = r.includes("-") ? r.split("-") : null;
|
||||
if (!range) {
|
||||
ERROR && console.error("Cannot parse the number. Check the format", r);
|
||||
return 0;
|
||||
}
|
||||
const count = rand(parseFloat(range[0]) * sign, +parseFloat(range[1]));
|
||||
if (isNaN(count) || count < 0) {
|
||||
if (Number.isNaN(count) || count < 0) {
|
||||
ERROR && console.error("Cannot parse number. Check the format", r);
|
||||
return 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Generate a random seed string
|
||||
* @return {string} random seed
|
||||
*/
|
||||
export const generateSeed = (): string => {
|
||||
return String(Math.floor(Math.random() * 1e9));
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -146,4 +159,4 @@ declare global {
|
|||
getNumberInRange: typeof getNumberInRange;
|
||||
generateSeed: typeof generateSeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { expect, describe, it } from 'vitest'
|
||||
import { round } from './stringUtils'
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { round } from "./stringUtils";
|
||||
|
||||
describe('round', () => {
|
||||
it('should be able to handle undefined input', () => {
|
||||
describe("round", () => {
|
||||
it("should be able to handle undefined input", () => {
|
||||
expect(round(undefined)).toBe("");
|
||||
});
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { rn } from "./numberUtils";
|
|||
* @returns {string} - The string with rounded numbers
|
||||
*/
|
||||
export const round = (inputString: string = "", decimals: number = 1) => {
|
||||
return inputString.replace(/[\d\.-][\d\.e-]*/g, (n: string) => {
|
||||
return inputString.replace(/[\d.-][\d.e-]*/g, (n: string) => {
|
||||
return rn(parseFloat(n), decimals).toString();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
|
|
@ -19,7 +19,7 @@ export const round = (inputString: string = "", decimals: number = 1) => {
|
|||
*/
|
||||
export const capitalize = (inputString: string) => {
|
||||
return inputString.charAt(0).toUpperCase() + inputString.slice(1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Split a string into two parts, trying to balance their lengths
|
||||
|
|
@ -46,13 +46,13 @@ export const splitInTwo = (inputString: string): string[] => {
|
|||
if (!last) return [first, middle];
|
||||
if (first.length < last.length) return [first + middle, last];
|
||||
return [first, middle + last];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse an SVG transform string into an array of numbers
|
||||
* @param {string} string - The SVG transform string
|
||||
* @returns {[number, number, number, number, number, number]} - The parsed transform as an array
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* parseTransform("matrix(1, 0, 0, 1, 100, 200)") // returns [1, 0, 0, 1, 100, 200]
|
||||
* parseTransform("translate(50, 75)") // returns [50, 75, 0, 0, 0, 1]
|
||||
|
|
@ -65,7 +65,7 @@ export const parseTransform = (string: string) => {
|
|||
.replace(/[ ]/g, ",")
|
||||
.split(",");
|
||||
return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a string is valid JSON
|
||||
|
|
@ -76,7 +76,7 @@ export const isValidJSON = (str: string): boolean => {
|
|||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -89,7 +89,7 @@ export const isValidJSON = (str: string): boolean => {
|
|||
export const safeParseJSON = (str: string) => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
@ -109,10 +109,10 @@ export const sanitizeId = (inputString: string) => {
|
|||
.replace(/\s+/g, "-"); // replace spaces with hyphens
|
||||
|
||||
// remove leading numbers
|
||||
if (sanitized.match(/^\d/)) sanitized = "_" + sanitized;
|
||||
if (sanitized.match(/^\d/)) sanitized = `_${sanitized}`;
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -122,4 +122,4 @@ declare global {
|
|||
parseTransform: typeof parseTransform;
|
||||
sanitizeId: typeof sanitizeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,19 +7,23 @@ type TemperatureScale = "°C" | "°F" | "K" | "°R" | "°De" | "°N" | "°Ré" |
|
|||
* @param {string} targetScale - Target temperature scale
|
||||
* @returns {string} - Converted temperature with unit
|
||||
*/
|
||||
export const convertTemperature = (temperatureInCelsius: number, targetScale: TemperatureScale = "°C") => {
|
||||
const temperatureConversionMap: {[key: string]: (temp: number) => string} = {
|
||||
"°C": (temp: number) => rn(temp) + "°C",
|
||||
"°F": (temp: number) => rn((temp * 9) / 5 + 32) + "°F",
|
||||
K: (temp: number) => rn(temp + 273.15) + "K",
|
||||
"°R": (temp: number) => rn(((temp + 273.15) * 9) / 5) + "°R",
|
||||
"°De": (temp: number) => rn(((100 - temp) * 3) / 2) + "°De",
|
||||
"°N": (temp: number) => rn((temp * 33) / 100) + "°N",
|
||||
"°Ré": (temp: number) => rn((temp * 4) / 5) + "°Ré",
|
||||
"°Rø": (temp: number) => rn((temp * 21) / 40 + 7.5) + "°Rø"
|
||||
};
|
||||
export const convertTemperature = (
|
||||
temperatureInCelsius: number,
|
||||
targetScale: TemperatureScale = "°C",
|
||||
) => {
|
||||
const temperatureConversionMap: { [key: string]: (temp: number) => string } =
|
||||
{
|
||||
"°C": (temp: number) => `${rn(temp)}°C`,
|
||||
"°F": (temp: number) => `${rn((temp * 9) / 5 + 32)}°F`,
|
||||
K: (temp: number) => `${rn(temp + 273.15)}K`,
|
||||
"°R": (temp: number) => `${rn(((temp + 273.15) * 9) / 5)}°R`,
|
||||
"°De": (temp: number) => `${rn(((100 - temp) * 3) / 2)}°De`,
|
||||
"°N": (temp: number) => `${rn((temp * 33) / 100)}°N`,
|
||||
"°Ré": (temp: number) => `${rn((temp * 4) / 5)}°Ré`,
|
||||
"°Rø": (temp: number) => `${rn((temp * 21) / 40 + 7.5)}°Rø`,
|
||||
};
|
||||
return temperatureConversionMap[targetScale](temperatureInCelsius);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert number to short string with SI postfix
|
||||
|
|
@ -27,13 +31,13 @@ export const convertTemperature = (temperatureInCelsius: number, targetScale: Te
|
|||
* @returns {string} - The converted string
|
||||
*/
|
||||
export const si = (n: number): string => {
|
||||
if (n >= 1e9) return rn(n / 1e9, 1) + "B";
|
||||
if (n >= 1e8) return rn(n / 1e6) + "M";
|
||||
if (n >= 1e6) return rn(n / 1e6, 1) + "M";
|
||||
if (n >= 1e4) return rn(n / 1e3) + "K";
|
||||
if (n >= 1e3) return rn(n / 1e3, 1) + "K";
|
||||
if (n >= 1e9) return `${rn(n / 1e9, 1)}B`;
|
||||
if (n >= 1e8) return `${rn(n / 1e6)}M`;
|
||||
if (n >= 1e6) return `${rn(n / 1e6, 1)}M`;
|
||||
if (n >= 1e4) return `${rn(n / 1e3)}K`;
|
||||
if (n >= 1e3) return `${rn(n / 1e3, 1)}K`;
|
||||
return rn(n).toString();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert string with SI postfix to integer
|
||||
|
|
@ -42,11 +46,11 @@ export const si = (n: number): string => {
|
|||
*/
|
||||
export const getIntegerFromSI = (value: string): number => {
|
||||
const metric = value.slice(-1);
|
||||
if (metric === "K") return parseInt(value.slice(0, -1)) * 1e3;
|
||||
if (metric === "M") return parseInt(value.slice(0, -1)) * 1e6;
|
||||
if (metric === "B") return parseInt(value.slice(0, -1)) * 1e9;
|
||||
return parseInt(value);
|
||||
}
|
||||
if (metric === "K") return parseInt(value.slice(0, -1), 10) * 1e3;
|
||||
if (metric === "M") return parseInt(value.slice(0, -1), 10) * 1e6;
|
||||
if (metric === "B") return parseInt(value.slice(0, -1), 10) * 1e9;
|
||||
return parseInt(value, 10);
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue