diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 698d23ae..c524a957 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,58 +1,76 @@
# Fantasy Map Generator
-Azgaar's Fantasy Map Generator is a client-side JavaScript web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
+Azgaar's Fantasy Map Generator is a web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
-- **CRITICAL**: This is a static web application - NO build process needed. No npm install, no compilation, no bundling.
-- Run the application using HTTP server (required - cannot run with file:// protocol):
- - `python3 -m http.server 8000` - takes 2-3 seconds to start
-- Access at: `http://localhost:8000`
+- The project uses NPM, Vite, and TypeScript for development and building.
+- **Setup**: Run `npm install` to install dependencies (requires Node.js >= 24.0.0)
+- **Development**: Run `npm run dev` to start the Vite development server
+ - Access at: `http://localhost:5173` (Vite's default port)
+ - Hot module replacement (HMR) enabled - changes are reflected immediately
+- **Building**: Run `npm run build` to compile TypeScript and build for production
+ - TypeScript compilation runs first (`tsc`)
+ - Vite builds the application to `dist/` directory
+- **Preview**: Run `npm run preview` to preview the production build locally
## Validation
- Always manually validate any changes by:
- 1. Starting the HTTP server (NEVER CANCEL - wait for full startup)
- 2. Navigate to the application in browser
+ 1. Run `npm run dev` to start the development server (wait for "ready" message)
+ 2. Navigate to the application in browser (typically `http://localhost:5173`)
3. Click the "►" button to open the menu and generate a new map
4. **CRITICAL VALIDATION**: Verify the map generates with countries, cities, roads, and geographic features
5. Test UI interaction: click "Layers" button, verify layer controls work
6. Test regeneration: click "New Map!" button, verify new map generates correctly
- **Known Issues**: Google Analytics and font loading errors are normal (blocked external resources)
+- For production build validation: run `npm run build` followed by `npm run preview`
## Repository Structure
### Core Files
-- `index.html` - Main application entry point
-- `main.js` - Core application logic
-- `versioning.js` - Version management and update handling
+- `package.json` - NPM package configuration with scripts and dependencies
+- `vite.config.ts` - Vite build configuration
+- `tsconfig.json` - TypeScript compiler configuration
-### Key Directories
+### Source Directories
-- `modules/` - core functionality modules:
- - `modules/ui/` - UI components (editors, tools, style management)
- - `modules/dynamic/` - runtime modules (export, installation)
- - `modules/renderers/` - drawing and rendering logic
-- `utils/` - utility libraries (math, arrays, strings, etc.)
-- `styles/` - visual style presets (JSON files)
-- `libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
-- `images/` - backgrounds, UI elements
-- `charges/` - heraldic symbols and coat of arms elements
-- `config/` - Heightmap templates and configurations
-- `heightmaps/` - Terrain generation data
+- `src/` - Source code directory (build input)
+ - `src/index.html` - Main application entry point
+ - `src/utils/` - TypeScript utility modules (migrated from JS)
+- `public/` - Static assets (copied to build output)
+ - `public/main.js` - Core application logic
+ - `public/versioning.js` - Version management and update handling
+ - `public/modules/` - Core functionality modules:
+ - `modules/ui/` - UI components (editors, tools, style management)
+ - `modules/dynamic/` - runtime modules (export, installation)
+ - `modules/renderers/` - drawing and rendering logic
+ - `public/styles/` - Visual style presets (JSON files)
+ - `public/libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
+ - `public/images/` - Backgrounds, UI elements
+ - `public/charges/` - Heraldic symbols and coat of arms elements
+ - `public/config/` - Heightmap templates and configurations
+ - `public/heightmaps/` - Terrain generation data
+- `dist/` - Production build output (generated by `npm run build`)
## Common Tasks
### Making Code Changes
-1. Edit JavaScript files directly (no compilation needed)
-2. Refresh browser to see changes immediately
-3. **ALWAYS test map generation** after making changes
-4. Update version in `versioning.js` for all changes
-5. Update file hashes in `index.html` for changed files (format: `file.js?v=1.108.1`)
+1. **TypeScript files** (`src/utils/*.ts`):
+ - Edit TypeScript files in the `src/utils/` directory
+ - Changes are automatically recompiled and hot-reloaded in dev mode
+ - Run `npm run build` to verify TypeScript compilation succeeds
+2. **JavaScript files** (`public/*.js`, `public/modules/*.js`):
+ - Edit JavaScript files directly in the `public/` directory
+ - Changes are automatically reflected in dev mode via HMR
+ - **Note**: Core application logic is still in JavaScript and gradually being migrated
+3. **Always test map generation** after making changes
+4. Update version in `public/versioning.js` for all changes
+5. For production builds, update file hashes in `src/index.html` if needed (format: `file.js?v=1.108.1`)
### Debugging Map Generation
@@ -71,19 +89,30 @@ Always reference these instructions first and fallback to search or bash command
### Application Won't Load
-- Ensure using HTTP server (not file://)
-- Check console for JavaScript errors
+- Run `npm install` to ensure dependencies are installed
+- Run `npm run dev` to start the development server
+- Check console for JavaScript/TypeScript errors
- Verify all files are present in repository
+- Ensure Node.js version >= 24.0.0 (`node --version`)
+
+### Build Failures
+
+- Check TypeScript compilation errors (`tsc` output)
+- Verify all dependencies are installed (`npm install`)
+- Check `tsconfig.json` for configuration issues
+- Look for import/module resolution errors
### Map Generation Fails
- Check browser console for error messages
- Look for specific module failures in generation logs
- Try refreshing page and generating new map
+- Verify build completed successfully if using production build
### Performance Issues
- Map generation should complete in ~1 second for standard configurations
- If slower, check browser console for errors
+- Development mode may be slower due to HMR overhead
-Remember: This is a sophisticated client-side application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. Always validate that your changes preserve the core map generation functionality.
+Remember: This is a sophisticated application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. The project is gradually migrating from vanilla JavaScript to TypeScript. Always validate that your changes preserve the core map generation functionality.
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..55bc8109
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,51 @@
+# Simple workflow for deploying static content to GitHub Pages
+name: Deploy static content to Pages
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: ['master']
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: 'pages'
+ cancel-in-progress: true
+
+jobs:
+ # Single deploy job since we're just deploying
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Set up Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: lts/*
+ cache: 'npm'
+ - name: Install dependencies
+ run: npm ci
+ - name: Build
+ run: npm run build
+ - name: Setup Pages
+ uses: actions/configure-pages@v5
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v4
+ with:
+ # Upload dist folder
+ path: './dist'
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index 6afdcc7d..00000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "version": "0.1.0",
- "configurations": [
- {
- "name": "Debug",
- "type": "chrome",
- "request": "launch",
- "file": "${workspaceFolder}/index.html"
- }
- ]
-}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index fbf5a425..58f9f058 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,27 @@
+# Build stage
+FROM node:24-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci
+
+# Copy source code
+COPY ./src ./src
+COPY ./public ./public
+COPY vite.config.js .
+
+# Build the application
+RUN npm run build
+
+# Production stage
FROM nginx:stable-alpine
-# Copy the contents of the repo to the container
-COPY . /usr/share/nginx/html
+# Copy built files from builder stage
+COPY --from=builder /app/dist /usr/share/nginx/html
-# Move the customized nginx config file to the nginx folder
-RUN mv /usr/share/nginx/html/.docker/default.conf /etc/nginx/conf.d/default.conf
+# Copy the customized nginx config file to the nginx folder
+COPY .docker/default.conf /etc/nginx/conf.d/default.conf
diff --git a/modules/heightmap-generator.js b/modules/heightmap-generator.js
deleted file mode 100644
index 87bc02d5..00000000
--- a/modules/heightmap-generator.js
+++ /dev/null
@@ -1,543 +0,0 @@
-"use strict";
-
-window.HeightmapGenerator = (function () {
- let grid = null;
- let heights = null;
- let blobPower;
- let linePower;
-
- const setGraph = graph => {
- const {cellsDesired, cells, points} = graph;
- heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length});
- blobPower = getBlobPower(cellsDesired);
- linePower = getLinePower(cellsDesired);
- grid = graph;
- };
-
- const getHeights = () => heights;
-
- const clearData = () => {
- heights = null;
- grid = null;
- };
-
- const fromTemplate = (graph, id) => {
- const templateString = heightmapTemplates[id]?.template || "";
- const steps = templateString.split("\n");
-
- if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
- setGraph(graph);
-
- for (const step of steps) {
- const elements = step.trim().split(" ");
- if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
- addStep(...elements);
- }
-
- return heights;
- };
-
- const fromPrecreated = (graph, id) => {
- return new Promise(resolve => {
- // create canvas where 1px corresponts to a cell
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- const {cellsX, cellsY} = graph;
- canvas.width = cellsX;
- canvas.height = cellsY;
-
- // load heightmap into image and render to canvas
- const img = new Image();
- img.src = `./heightmaps/${id}.png`;
- img.onload = () => {
- ctx.drawImage(img, 0, 0, cellsX, cellsY);
- const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
- setGraph(graph);
- getHeightsFromImageData(imageData.data);
- canvas.remove();
- img.remove();
- resolve(heights);
- };
- });
- };
-
- const generate = async function (graph) {
- TIME && console.time("defineHeightmap");
- const id = byId("templateInput").value;
-
- Math.random = aleaPRNG(seed);
- const isTemplate = id in heightmapTemplates;
- const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
- TIME && console.timeEnd("defineHeightmap");
-
- clearData();
- return heights;
- };
-
- function addStep(tool, a2, a3, a4, a5) {
- if (tool === "Hill") return addHill(a2, a3, a4, a5);
- if (tool === "Pit") return addPit(a2, a3, a4, a5);
- if (tool === "Range") return addRange(a2, a3, a4, a5);
- if (tool === "Trough") return addTrough(a2, a3, a4, a5);
- if (tool === "Strait") return addStrait(a2, a3);
- if (tool === "Mask") return mask(a2);
- if (tool === "Invert") return invert(a2, a3);
- if (tool === "Add") return modify(a3, +a2, 1);
- if (tool === "Multiply") return modify(a3, 0, +a2);
- if (tool === "Smooth") return smooth(a2);
- }
-
- function getBlobPower(cells) {
- const blobPowerMap = {
- 1000: 0.93,
- 2000: 0.95,
- 5000: 0.97,
- 10000: 0.98,
- 20000: 0.99,
- 30000: 0.991,
- 40000: 0.993,
- 50000: 0.994,
- 60000: 0.995,
- 70000: 0.9955,
- 80000: 0.996,
- 90000: 0.9964,
- 100000: 0.9973
- };
- return blobPowerMap[cells] || 0.98;
- }
-
- function getLinePower(cells) {
- const linePowerMap = {
- 1000: 0.75,
- 2000: 0.77,
- 5000: 0.79,
- 10000: 0.81,
- 20000: 0.82,
- 30000: 0.83,
- 40000: 0.84,
- 50000: 0.86,
- 60000: 0.87,
- 70000: 0.88,
- 80000: 0.91,
- 90000: 0.92,
- 100000: 0.93
- };
-
- return linePowerMap[cells] || 0.81;
- }
-
- const addHill = (count, height, rangeX, rangeY) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOneHill();
- count--;
- }
-
- function addOneHill() {
- const change = new Uint8Array(heights.length);
- let limit = 0;
- let start;
- let h = lim(getNumberInRange(height));
-
- do {
- const x = getPointInRange(rangeX, graphWidth);
- const y = getPointInRange(rangeY, graphHeight);
- start = findGridCell(x, y, grid);
- limit++;
- } while (heights[start] + h > 90 && limit < 50);
-
- change[start] = h;
- const queue = [start];
- while (queue.length) {
- const q = queue.shift();
-
- for (const c of grid.cells.c[q]) {
- if (change[c]) continue;
- change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
- if (change[c] > 1) queue.push(c);
- }
- }
-
- heights = heights.map((h, i) => lim(h + change[i]));
- }
- };
-
- const addPit = (count, height, rangeX, rangeY) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOnePit();
- count--;
- }
-
- function addOnePit() {
- const used = new Uint8Array(heights.length);
- let limit = 0,
- start;
- let h = lim(getNumberInRange(height));
-
- do {
- const x = getPointInRange(rangeX, graphWidth);
- const y = getPointInRange(rangeY, graphHeight);
- start = findGridCell(x, y, grid);
- limit++;
- } while (heights[start] < 20 && limit < 50);
-
- const queue = [start];
- while (queue.length) {
- const q = queue.shift();
- h = h ** blobPower * (Math.random() * 0.2 + 0.9);
- if (h < 1) return;
-
- grid.cells.c[q].forEach(function (c, i) {
- if (used[c]) return;
- heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
- used[c] = 1;
- queue.push(c);
- });
- }
- }
- };
-
- // fromCell, toCell are options cell ids
- const addRange = (count, height, rangeX, rangeY, startCell, endCell) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOneRange();
- count--;
- }
-
- function addOneRange() {
- const used = new Uint8Array(heights.length);
- let h = lim(getNumberInRange(height));
-
- if (rangeX && rangeY) {
- // find start and end points
- const startX = getPointInRange(rangeX, graphWidth);
- const startY = getPointInRange(rangeY, graphHeight);
-
- let dist = 0,
- limit = 0,
- endX,
- endY;
-
- do {
- endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
- endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
- dist = Math.abs(endY - startY) + Math.abs(endX - startX);
- limit++;
- } while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
-
- startCell = findGridCell(startX, startY, grid);
- endCell = findGridCell(endX, endY, grid);
- }
-
- let range = getRange(startCell, endCell);
-
- // get main ridge
- function getRange(cur, end) {
- const range = [cur];
- const p = grid.points;
- used[cur] = 1;
-
- while (cur !== end) {
- let min = Infinity;
- grid.cells.c[cur].forEach(function (e) {
- if (used[e]) return;
- let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
- if (Math.random() > 0.85) diff = diff / 2;
- if (diff < min) {
- min = diff;
- cur = e;
- }
- });
- if (min === Infinity) return range;
- range.push(cur);
- used[cur] = 1;
- }
-
- return range;
- }
-
- // add height to ridge and cells around
- let queue = range.slice(),
- i = 0;
- while (queue.length) {
- const frontier = queue.slice();
- (queue = []), i++;
- frontier.forEach(i => {
- heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
- });
- h = h ** linePower - 1;
- if (h < 2) break;
- frontier.forEach(f => {
- grid.cells.c[f].forEach(i => {
- if (!used[i]) {
- queue.push(i);
- used[i] = 1;
- }
- });
- });
- }
-
- // generate prominences
- range.forEach((cur, d) => {
- if (d % 6 !== 0) return;
- for (const l of d3.range(i)) {
- const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
- heights[min] = (heights[cur] * 2 + heights[min]) / 3;
- cur = min;
- }
- });
- }
- };
-
- const addTrough = (count, height, rangeX, rangeY, startCell, endCell) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOneTrough();
- count--;
- }
-
- function addOneTrough() {
- const used = new Uint8Array(heights.length);
- let h = lim(getNumberInRange(height));
-
- if (rangeX && rangeY) {
- // find start and end points
- let limit = 0,
- startX,
- startY,
- dist = 0,
- endX,
- endY;
- do {
- startX = getPointInRange(rangeX, graphWidth);
- startY = getPointInRange(rangeY, graphHeight);
- startCell = findGridCell(startX, startY, grid);
- limit++;
- } while (heights[startCell] < 20 && limit < 50);
-
- limit = 0;
- do {
- endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
- endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
- dist = Math.abs(endY - startY) + Math.abs(endX - startX);
- limit++;
- } while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
-
- endCell = findGridCell(endX, endY, grid);
- }
-
- let range = getRange(startCell, endCell);
-
- // get main ridge
- function getRange(cur, end) {
- const range = [cur];
- const p = grid.points;
- used[cur] = 1;
-
- while (cur !== end) {
- let min = Infinity;
- grid.cells.c[cur].forEach(function (e) {
- if (used[e]) return;
- let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
- if (Math.random() > 0.8) diff = diff / 2;
- if (diff < min) {
- min = diff;
- cur = e;
- }
- });
- if (min === Infinity) return range;
- range.push(cur);
- used[cur] = 1;
- }
-
- return range;
- }
-
- // add height to ridge and cells around
- let queue = range.slice(),
- i = 0;
- while (queue.length) {
- const frontier = queue.slice();
- (queue = []), i++;
- frontier.forEach(i => {
- heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
- });
- h = h ** linePower - 1;
- if (h < 2) break;
- frontier.forEach(f => {
- grid.cells.c[f].forEach(i => {
- if (!used[i]) {
- queue.push(i);
- used[i] = 1;
- }
- });
- });
- }
-
- // generate prominences
- range.forEach((cur, d) => {
- if (d % 6 !== 0) return;
- for (const l of d3.range(i)) {
- const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
- //debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
- heights[min] = (heights[cur] * 2 + heights[min]) / 3;
- cur = min;
- }
- });
- }
- };
-
- const addStrait = (width, direction = "vertical") => {
- width = Math.min(getNumberInRange(width), grid.cellsX / 3);
- if (width < 1 && P(width)) return;
- const used = new Uint8Array(heights.length);
- const vert = direction === "vertical";
- const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
- const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
- const endX = vert
- ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2)
- : graphWidth - 5;
- const endY = vert
- ? graphHeight - 5
- : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
-
- const start = findGridCell(startX, startY, grid);
- const end = findGridCell(endX, endY, grid);
- let range = getRange(start, end);
- const query = [];
-
- function getRange(cur, end) {
- const range = [];
- const p = grid.points;
-
- while (cur !== end) {
- let min = Infinity;
- grid.cells.c[cur].forEach(function (e) {
- let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
- if (Math.random() > 0.8) diff = diff / 2;
- if (diff < min) {
- min = diff;
- cur = e;
- }
- });
- range.push(cur);
- }
-
- return range;
- }
-
- const step = 0.1 / width;
-
- while (width > 0) {
- const exp = 0.9 - step * width;
- range.forEach(function (r) {
- grid.cells.c[r].forEach(function (e) {
- if (used[e]) return;
- used[e] = 1;
- query.push(e);
- heights[e] **= exp;
- if (heights[e] > 100) heights[e] = 5;
- });
- });
- range = query.slice();
-
- width--;
- }
- };
-
- const modify = (range, add, mult, power) => {
- const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
- const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
- const isLand = min === 20;
-
- heights = heights.map(h => {
- if (h < min || h > max) return h;
-
- if (add) h = isLand ? Math.max(h + add, 20) : h + add;
- if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
- if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
- return lim(h);
- });
- };
-
- const smooth = (fr = 2, add = 0) => {
- heights = heights.map((h, i) => {
- const a = [h];
- grid.cells.c[i].forEach(c => a.push(heights[c]));
- if (fr === 1) return d3.mean(a) + add;
- return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
- });
- };
-
- const mask = (power = 1) => {
- const fr = power ? Math.abs(power) : 1;
-
- heights = heights.map((h, i) => {
- const [x, y] = grid.points[i];
- const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
- const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
- let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
- if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
- const masked = h * distance;
- return lim((h * (fr - 1) + masked) / fr);
- });
- };
-
- const invert = (count, axes) => {
- if (!P(count)) return;
-
- const invertX = axes !== "y";
- const invertY = axes !== "x";
- const {cellsX, cellsY} = grid;
-
- const inverted = heights.map((h, i) => {
- const x = i % cellsX;
- const y = Math.floor(i / cellsX);
-
- const nx = invertX ? cellsX - x - 1 : x;
- const ny = invertY ? cellsY - y - 1 : y;
- const invertedI = nx + ny * cellsX;
- return heights[invertedI];
- });
-
- heights = inverted;
- };
-
- function getPointInRange(range, length) {
- if (typeof range !== "string") {
- ERROR && console.error("Range should be a string");
- return;
- }
-
- const min = range.split("-")[0] / 100 || 0;
- const max = range.split("-")[1] / 100 || min;
- return rand(min * length, max * length);
- }
-
- function getHeightsFromImageData(imageData) {
- for (let i = 0; i < heights.length; i++) {
- const lightness = imageData[i * 4] / 255;
- const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
- heights[i] = minmax(Math.floor(powered * 100), 0, 100);
- }
- }
-
- return {
- setGraph,
- getHeights,
- generate,
- fromTemplate,
- fromPrecreated,
- addHill,
- addRange,
- addTrough,
- addStrait,
- addPit,
- smooth,
- modify,
- mask,
- invert
- };
-})();
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 00000000..2cf2371b
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,9 @@
+[build]
+ command = "npm run build"
+ publish = "dist"
+ environment = { NODE_VERSION = "24" }
+
+[[redirects]]
+ from = "/*"
+ to = "/index.html"
+ status = 200
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..39e699c9
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1916 @@
+{
+ "name": "fantasy-map-generator",
+ "version": "1.109.5",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fantasy-map-generator",
+ "version": "1.109.5",
+ "license": "MIT",
+ "dependencies": {
+ "alea": "^1.0.1",
+ "d3": "^7.9.0",
+ "delaunator": "^5.0.1",
+ "polylabel": "^2.0.1"
+ },
+ "devDependencies": {
+ "@types/d3": "^7.4.3",
+ "@types/delaunator": "^5.0.3",
+ "@types/polylabel": "^1.1.3",
+ "typescript": "^5.9.3",
+ "vite": "^7.3.1"
+ },
+ "engines": {
+ "node": ">=24.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+ "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+ "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+ "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+ "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+ "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+ "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+ "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+ "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+ "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+ "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+ "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+ "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+ "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+ "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+ "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+ "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+ "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+ "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+ "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+ "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+ "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/delaunator": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@types/delaunator/-/delaunator-5.0.3.tgz",
+ "integrity": "sha512-6tTLP8NX0OwtB/fmW9bXp4EWPptawTSsrSGjboWRuzqkxNEEJGyzRPHbr8wnV2DBWfAZ+EPTOvW3B/KysJrl2g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/polylabel": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz",
+ "integrity": "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/alea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
+ "integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==",
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/polylabel": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-2.0.1.tgz",
+ "integrity": "sha512-B6Yu+Bdl/8SGtjVhyUfZzD3DwciCS9SPVtHiNdt8idHHatvTHp5Ss8XGDRmQFtfF1ZQnfK+Cj5dXdpkUXBbXgA==",
+ "license": "ISC",
+ "dependencies": {
+ "tinyqueue": "^3.0.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
+ "license": "Unlicense"
+ },
+ "node_modules/rollup": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
+ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.55.1",
+ "@rollup/rollup-android-arm64": "4.55.1",
+ "@rollup/rollup-darwin-arm64": "4.55.1",
+ "@rollup/rollup-darwin-x64": "4.55.1",
+ "@rollup/rollup-freebsd-arm64": "4.55.1",
+ "@rollup/rollup-freebsd-x64": "4.55.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+ "@rollup/rollup-linux-arm64-musl": "4.55.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+ "@rollup/rollup-linux-loong64-musl": "4.55.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-musl": "4.55.1",
+ "@rollup/rollup-openbsd-x64": "4.55.1",
+ "@rollup/rollup-openharmony-arm64": "4.55.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+ "@rollup/rollup-win32-x64-gnu": "4.55.1",
+ "@rollup/rollup-win32-x64-msvc": "4.55.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyqueue": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
+ "license": "ISC"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..cadffbd4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "fantasy-map-generator",
+ "version": "1.109.5",
+ "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.",
+ "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme",
+ "bugs": {
+ "url": "https://github.com/Azgaar/Fantasy-Map-Generator/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Azgaar/Fantasy-Map-Generator.git"
+ },
+ "license": "MIT",
+ "author": "Azgaar",
+ "main": "main.js",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@types/d3": "^7.4.3",
+ "@types/delaunator": "^5.0.3",
+ "@types/polylabel": "^1.1.3",
+ "typescript": "^5.9.3",
+ "vite": "^7.3.1"
+ },
+ "dependencies": {
+ "alea": "^1.0.1",
+ "d3": "^7.9.0",
+ "delaunator": "^5.0.1",
+ "polylabel": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=24.0.0"
+ }
+}
diff --git a/charges/agnusDei.svg b/public/charges/agnusDei.svg
similarity index 100%
rename from charges/agnusDei.svg
rename to public/charges/agnusDei.svg
diff --git a/charges/anchor.svg b/public/charges/anchor.svg
similarity index 100%
rename from charges/anchor.svg
rename to public/charges/anchor.svg
diff --git a/charges/angel.svg b/public/charges/angel.svg
similarity index 100%
rename from charges/angel.svg
rename to public/charges/angel.svg
diff --git a/charges/annulet.svg b/public/charges/annulet.svg
similarity index 100%
rename from charges/annulet.svg
rename to public/charges/annulet.svg
diff --git a/charges/anvil.svg b/public/charges/anvil.svg
similarity index 100%
rename from charges/anvil.svg
rename to public/charges/anvil.svg
diff --git a/charges/apple.svg b/public/charges/apple.svg
similarity index 100%
rename from charges/apple.svg
rename to public/charges/apple.svg
diff --git a/charges/arbalest.svg b/public/charges/arbalest.svg
similarity index 100%
rename from charges/arbalest.svg
rename to public/charges/arbalest.svg
diff --git a/charges/arbalest2.svg b/public/charges/arbalest2.svg
similarity index 100%
rename from charges/arbalest2.svg
rename to public/charges/arbalest2.svg
diff --git a/charges/archer.svg b/public/charges/archer.svg
similarity index 100%
rename from charges/archer.svg
rename to public/charges/archer.svg
diff --git a/charges/armEmbowedHoldingSabre.svg b/public/charges/armEmbowedHoldingSabre.svg
similarity index 100%
rename from charges/armEmbowedHoldingSabre.svg
rename to public/charges/armEmbowedHoldingSabre.svg
diff --git a/charges/armEmbowedVambraced.svg b/public/charges/armEmbowedVambraced.svg
similarity index 100%
rename from charges/armEmbowedVambraced.svg
rename to public/charges/armEmbowedVambraced.svg
diff --git a/charges/armEmbowedVambracedHoldingSword.svg b/public/charges/armEmbowedVambracedHoldingSword.svg
similarity index 100%
rename from charges/armEmbowedVambracedHoldingSword.svg
rename to public/charges/armEmbowedVambracedHoldingSword.svg
diff --git a/charges/armillarySphere.svg b/public/charges/armillarySphere.svg
similarity index 100%
rename from charges/armillarySphere.svg
rename to public/charges/armillarySphere.svg
diff --git a/charges/arrow.svg b/public/charges/arrow.svg
similarity index 100%
rename from charges/arrow.svg
rename to public/charges/arrow.svg
diff --git a/charges/arrowsSheaf.svg b/public/charges/arrowsSheaf.svg
similarity index 100%
rename from charges/arrowsSheaf.svg
rename to public/charges/arrowsSheaf.svg
diff --git a/charges/attire.svg b/public/charges/attire.svg
similarity index 100%
rename from charges/attire.svg
rename to public/charges/attire.svg
diff --git a/charges/axe.svg b/public/charges/axe.svg
similarity index 100%
rename from charges/axe.svg
rename to public/charges/axe.svg
diff --git a/charges/badgerStatant.svg b/public/charges/badgerStatant.svg
similarity index 100%
rename from charges/badgerStatant.svg
rename to public/charges/badgerStatant.svg
diff --git a/charges/banner.svg b/public/charges/banner.svg
similarity index 100%
rename from charges/banner.svg
rename to public/charges/banner.svg
diff --git a/charges/basilisk.svg b/public/charges/basilisk.svg
similarity index 100%
rename from charges/basilisk.svg
rename to public/charges/basilisk.svg
diff --git a/charges/bearPassant.svg b/public/charges/bearPassant.svg
similarity index 100%
rename from charges/bearPassant.svg
rename to public/charges/bearPassant.svg
diff --git a/charges/bearRampant.svg b/public/charges/bearRampant.svg
similarity index 100%
rename from charges/bearRampant.svg
rename to public/charges/bearRampant.svg
diff --git a/charges/bee.svg b/public/charges/bee.svg
similarity index 100%
rename from charges/bee.svg
rename to public/charges/bee.svg
diff --git a/charges/bell.svg b/public/charges/bell.svg
similarity index 100%
rename from charges/bell.svg
rename to public/charges/bell.svg
diff --git a/charges/billet.svg b/public/charges/billet.svg
similarity index 100%
rename from charges/billet.svg
rename to public/charges/billet.svg
diff --git a/charges/boarHeadErased.svg b/public/charges/boarHeadErased.svg
similarity index 100%
rename from charges/boarHeadErased.svg
rename to public/charges/boarHeadErased.svg
diff --git a/charges/boarRampant.svg b/public/charges/boarRampant.svg
similarity index 100%
rename from charges/boarRampant.svg
rename to public/charges/boarRampant.svg
diff --git a/charges/boat.svg b/public/charges/boat.svg
similarity index 100%
rename from charges/boat.svg
rename to public/charges/boat.svg
diff --git a/charges/boat2.svg b/public/charges/boat2.svg
similarity index 100%
rename from charges/boat2.svg
rename to public/charges/boat2.svg
diff --git a/charges/bone.svg b/public/charges/bone.svg
similarity index 100%
rename from charges/bone.svg
rename to public/charges/bone.svg
diff --git a/charges/bookClosed.svg b/public/charges/bookClosed.svg
similarity index 100%
rename from charges/bookClosed.svg
rename to public/charges/bookClosed.svg
diff --git a/charges/bookClosed2.svg b/public/charges/bookClosed2.svg
similarity index 100%
rename from charges/bookClosed2.svg
rename to public/charges/bookClosed2.svg
diff --git a/charges/bookOpen.svg b/public/charges/bookOpen.svg
similarity index 100%
rename from charges/bookOpen.svg
rename to public/charges/bookOpen.svg
diff --git a/charges/bow.svg b/public/charges/bow.svg
similarity index 100%
rename from charges/bow.svg
rename to public/charges/bow.svg
diff --git a/charges/bowWithArrow.svg b/public/charges/bowWithArrow.svg
similarity index 100%
rename from charges/bowWithArrow.svg
rename to public/charges/bowWithArrow.svg
diff --git a/charges/bowWithThreeArrows.svg b/public/charges/bowWithThreeArrows.svg
similarity index 100%
rename from charges/bowWithThreeArrows.svg
rename to public/charges/bowWithThreeArrows.svg
diff --git a/charges/bridge.svg b/public/charges/bridge.svg
similarity index 100%
rename from charges/bridge.svg
rename to public/charges/bridge.svg
diff --git a/charges/bridge2.svg b/public/charges/bridge2.svg
similarity index 100%
rename from charges/bridge2.svg
rename to public/charges/bridge2.svg
diff --git a/charges/bucket.svg b/public/charges/bucket.svg
similarity index 100%
rename from charges/bucket.svg
rename to public/charges/bucket.svg
diff --git a/charges/buckle.svg b/public/charges/buckle.svg
similarity index 100%
rename from charges/buckle.svg
rename to public/charges/buckle.svg
diff --git a/charges/bugleHorn.svg b/public/charges/bugleHorn.svg
similarity index 100%
rename from charges/bugleHorn.svg
rename to public/charges/bugleHorn.svg
diff --git a/charges/bugleHorn2.svg b/public/charges/bugleHorn2.svg
similarity index 100%
rename from charges/bugleHorn2.svg
rename to public/charges/bugleHorn2.svg
diff --git a/charges/bullHeadCaboshed.svg b/public/charges/bullHeadCaboshed.svg
similarity index 100%
rename from charges/bullHeadCaboshed.svg
rename to public/charges/bullHeadCaboshed.svg
diff --git a/charges/bullPassant.svg b/public/charges/bullPassant.svg
similarity index 100%
rename from charges/bullPassant.svg
rename to public/charges/bullPassant.svg
diff --git a/charges/butterfly.svg b/public/charges/butterfly.svg
similarity index 100%
rename from charges/butterfly.svg
rename to public/charges/butterfly.svg
diff --git a/charges/camel.svg b/public/charges/camel.svg
similarity index 100%
rename from charges/camel.svg
rename to public/charges/camel.svg
diff --git a/charges/cancer.svg b/public/charges/cancer.svg
similarity index 100%
rename from charges/cancer.svg
rename to public/charges/cancer.svg
diff --git a/charges/cannon.svg b/public/charges/cannon.svg
similarity index 100%
rename from charges/cannon.svg
rename to public/charges/cannon.svg
diff --git a/charges/caravel.svg b/public/charges/caravel.svg
similarity index 100%
rename from charges/caravel.svg
rename to public/charges/caravel.svg
diff --git a/charges/carreau.svg b/public/charges/carreau.svg
similarity index 100%
rename from charges/carreau.svg
rename to public/charges/carreau.svg
diff --git a/charges/castle.svg b/public/charges/castle.svg
similarity index 100%
rename from charges/castle.svg
rename to public/charges/castle.svg
diff --git a/charges/castle2.svg b/public/charges/castle2.svg
similarity index 100%
rename from charges/castle2.svg
rename to public/charges/castle2.svg
diff --git a/charges/catPassantGuardant.svg b/public/charges/catPassantGuardant.svg
similarity index 100%
rename from charges/catPassantGuardant.svg
rename to public/charges/catPassantGuardant.svg
diff --git a/charges/cavalier.svg b/public/charges/cavalier.svg
similarity index 100%
rename from charges/cavalier.svg
rename to public/charges/cavalier.svg
diff --git a/charges/centaur.svg b/public/charges/centaur.svg
similarity index 100%
rename from charges/centaur.svg
rename to public/charges/centaur.svg
diff --git a/charges/chain.svg b/public/charges/chain.svg
similarity index 100%
rename from charges/chain.svg
rename to public/charges/chain.svg
diff --git a/charges/chalice.svg b/public/charges/chalice.svg
similarity index 100%
rename from charges/chalice.svg
rename to public/charges/chalice.svg
diff --git a/charges/cinquefoil.svg b/public/charges/cinquefoil.svg
similarity index 100%
rename from charges/cinquefoil.svg
rename to public/charges/cinquefoil.svg
diff --git a/charges/cock.svg b/public/charges/cock.svg
similarity index 100%
rename from charges/cock.svg
rename to public/charges/cock.svg
diff --git a/charges/column.svg b/public/charges/column.svg
similarity index 100%
rename from charges/column.svg
rename to public/charges/column.svg
diff --git a/charges/comet.svg b/public/charges/comet.svg
similarity index 100%
rename from charges/comet.svg
rename to public/charges/comet.svg
diff --git a/charges/compassRose.svg b/public/charges/compassRose.svg
similarity index 100%
rename from charges/compassRose.svg
rename to public/charges/compassRose.svg
diff --git a/charges/cossack.svg b/public/charges/cossack.svg
similarity index 100%
rename from charges/cossack.svg
rename to public/charges/cossack.svg
diff --git a/charges/cowHorns.svg b/public/charges/cowHorns.svg
similarity index 100%
rename from charges/cowHorns.svg
rename to public/charges/cowHorns.svg
diff --git a/charges/cowStatant.svg b/public/charges/cowStatant.svg
similarity index 100%
rename from charges/cowStatant.svg
rename to public/charges/cowStatant.svg
diff --git a/charges/crescent.svg b/public/charges/crescent.svg
similarity index 100%
rename from charges/crescent.svg
rename to public/charges/crescent.svg
diff --git a/charges/crocodile.svg b/public/charges/crocodile.svg
similarity index 100%
rename from charges/crocodile.svg
rename to public/charges/crocodile.svg
diff --git a/charges/crosier.svg b/public/charges/crosier.svg
similarity index 100%
rename from charges/crosier.svg
rename to public/charges/crosier.svg
diff --git a/charges/crossAnkh.svg b/public/charges/crossAnkh.svg
similarity index 100%
rename from charges/crossAnkh.svg
rename to public/charges/crossAnkh.svg
diff --git a/charges/crossArrowed.svg b/public/charges/crossArrowed.svg
similarity index 100%
rename from charges/crossArrowed.svg
rename to public/charges/crossArrowed.svg
diff --git a/charges/crossAvellane.svg b/public/charges/crossAvellane.svg
similarity index 100%
rename from charges/crossAvellane.svg
rename to public/charges/crossAvellane.svg
diff --git a/charges/crossBiparted.svg b/public/charges/crossBiparted.svg
similarity index 100%
rename from charges/crossBiparted.svg
rename to public/charges/crossBiparted.svg
diff --git a/charges/crossBottony.svg b/public/charges/crossBottony.svg
similarity index 100%
rename from charges/crossBottony.svg
rename to public/charges/crossBottony.svg
diff --git a/charges/crossBurgundy.svg b/public/charges/crossBurgundy.svg
similarity index 100%
rename from charges/crossBurgundy.svg
rename to public/charges/crossBurgundy.svg
diff --git a/charges/crossCalvary.svg b/public/charges/crossCalvary.svg
similarity index 100%
rename from charges/crossCalvary.svg
rename to public/charges/crossCalvary.svg
diff --git a/charges/crossCarolingian.svg b/public/charges/crossCarolingian.svg
similarity index 100%
rename from charges/crossCarolingian.svg
rename to public/charges/crossCarolingian.svg
diff --git a/charges/crossCeltic.svg b/public/charges/crossCeltic.svg
similarity index 100%
rename from charges/crossCeltic.svg
rename to public/charges/crossCeltic.svg
diff --git a/charges/crossCeltic2.svg b/public/charges/crossCeltic2.svg
similarity index 100%
rename from charges/crossCeltic2.svg
rename to public/charges/crossCeltic2.svg
diff --git a/charges/crossCercelee.svg b/public/charges/crossCercelee.svg
similarity index 100%
rename from charges/crossCercelee.svg
rename to public/charges/crossCercelee.svg
diff --git a/charges/crossClechy.svg b/public/charges/crossClechy.svg
similarity index 100%
rename from charges/crossClechy.svg
rename to public/charges/crossClechy.svg
diff --git a/charges/crossDouble.svg b/public/charges/crossDouble.svg
similarity index 100%
rename from charges/crossDouble.svg
rename to public/charges/crossDouble.svg
diff --git a/charges/crossErminee.svg b/public/charges/crossErminee.svg
similarity index 100%
rename from charges/crossErminee.svg
rename to public/charges/crossErminee.svg
diff --git a/charges/crossFitchy.svg b/public/charges/crossFitchy.svg
similarity index 100%
rename from charges/crossFitchy.svg
rename to public/charges/crossFitchy.svg
diff --git a/charges/crossFleury.svg b/public/charges/crossFleury.svg
similarity index 100%
rename from charges/crossFleury.svg
rename to public/charges/crossFleury.svg
diff --git a/charges/crossFormee.svg b/public/charges/crossFormee.svg
similarity index 100%
rename from charges/crossFormee.svg
rename to public/charges/crossFormee.svg
diff --git a/charges/crossFormee2.svg b/public/charges/crossFormee2.svg
similarity index 100%
rename from charges/crossFormee2.svg
rename to public/charges/crossFormee2.svg
diff --git a/charges/crossFourchy.svg b/public/charges/crossFourchy.svg
similarity index 100%
rename from charges/crossFourchy.svg
rename to public/charges/crossFourchy.svg
diff --git a/charges/crossGamma.svg b/public/charges/crossGamma.svg
similarity index 100%
rename from charges/crossGamma.svg
rename to public/charges/crossGamma.svg
diff --git a/charges/crossHummetty.svg b/public/charges/crossHummetty.svg
similarity index 100%
rename from charges/crossHummetty.svg
rename to public/charges/crossHummetty.svg
diff --git a/charges/crossJerusalem.svg b/public/charges/crossJerusalem.svg
similarity index 100%
rename from charges/crossJerusalem.svg
rename to public/charges/crossJerusalem.svg
diff --git a/charges/crossLatin.svg b/public/charges/crossLatin.svg
similarity index 100%
rename from charges/crossLatin.svg
rename to public/charges/crossLatin.svg
diff --git a/charges/crossMaltese.svg b/public/charges/crossMaltese.svg
similarity index 100%
rename from charges/crossMaltese.svg
rename to public/charges/crossMaltese.svg
diff --git a/charges/crossMoline.svg b/public/charges/crossMoline.svg
similarity index 100%
rename from charges/crossMoline.svg
rename to public/charges/crossMoline.svg
diff --git a/charges/crossOccitan.svg b/public/charges/crossOccitan.svg
similarity index 100%
rename from charges/crossOccitan.svg
rename to public/charges/crossOccitan.svg
diff --git a/charges/crossOrthodox.svg b/public/charges/crossOrthodox.svg
similarity index 100%
rename from charges/crossOrthodox.svg
rename to public/charges/crossOrthodox.svg
diff --git a/charges/crossPatonce.svg b/public/charges/crossPatonce.svg
similarity index 100%
rename from charges/crossPatonce.svg
rename to public/charges/crossPatonce.svg
diff --git a/charges/crossPatriarchal.svg b/public/charges/crossPatriarchal.svg
similarity index 100%
rename from charges/crossPatriarchal.svg
rename to public/charges/crossPatriarchal.svg
diff --git a/charges/crossPattee.svg b/public/charges/crossPattee.svg
similarity index 100%
rename from charges/crossPattee.svg
rename to public/charges/crossPattee.svg
diff --git a/charges/crossPatteeAlisee.svg b/public/charges/crossPatteeAlisee.svg
similarity index 100%
rename from charges/crossPatteeAlisee.svg
rename to public/charges/crossPatteeAlisee.svg
diff --git a/charges/crossPommy.svg b/public/charges/crossPommy.svg
similarity index 100%
rename from charges/crossPommy.svg
rename to public/charges/crossPommy.svg
diff --git a/charges/crossPotent.svg b/public/charges/crossPotent.svg
similarity index 100%
rename from charges/crossPotent.svg
rename to public/charges/crossPotent.svg
diff --git a/charges/crossSaltire.svg b/public/charges/crossSaltire.svg
similarity index 100%
rename from charges/crossSaltire.svg
rename to public/charges/crossSaltire.svg
diff --git a/charges/crossSantiago.svg b/public/charges/crossSantiago.svg
similarity index 100%
rename from charges/crossSantiago.svg
rename to public/charges/crossSantiago.svg
diff --git a/charges/crossTau.svg b/public/charges/crossTau.svg
similarity index 100%
rename from charges/crossTau.svg
rename to public/charges/crossTau.svg
diff --git a/charges/crossTemplar.svg b/public/charges/crossTemplar.svg
similarity index 100%
rename from charges/crossTemplar.svg
rename to public/charges/crossTemplar.svg
diff --git a/charges/crossTriquetra.svg b/public/charges/crossTriquetra.svg
similarity index 100%
rename from charges/crossTriquetra.svg
rename to public/charges/crossTriquetra.svg
diff --git a/charges/crossVoided.svg b/public/charges/crossVoided.svg
similarity index 100%
rename from charges/crossVoided.svg
rename to public/charges/crossVoided.svg
diff --git a/charges/crossedBones.svg b/public/charges/crossedBones.svg
similarity index 100%
rename from charges/crossedBones.svg
rename to public/charges/crossedBones.svg
diff --git a/charges/crosslet.svg b/public/charges/crosslet.svg
similarity index 100%
rename from charges/crosslet.svg
rename to public/charges/crosslet.svg
diff --git a/charges/crown.svg b/public/charges/crown.svg
similarity index 100%
rename from charges/crown.svg
rename to public/charges/crown.svg
diff --git a/charges/crown2.svg b/public/charges/crown2.svg
similarity index 100%
rename from charges/crown2.svg
rename to public/charges/crown2.svg
diff --git a/charges/deerHeadCaboshed.svg b/public/charges/deerHeadCaboshed.svg
similarity index 100%
rename from charges/deerHeadCaboshed.svg
rename to public/charges/deerHeadCaboshed.svg
diff --git a/charges/delf.svg b/public/charges/delf.svg
similarity index 100%
rename from charges/delf.svg
rename to public/charges/delf.svg
diff --git a/charges/dolphin.svg b/public/charges/dolphin.svg
similarity index 100%
rename from charges/dolphin.svg
rename to public/charges/dolphin.svg
diff --git a/charges/donkeyHeadCaboshed.svg b/public/charges/donkeyHeadCaboshed.svg
similarity index 100%
rename from charges/donkeyHeadCaboshed.svg
rename to public/charges/donkeyHeadCaboshed.svg
diff --git a/charges/dove.svg b/public/charges/dove.svg
similarity index 100%
rename from charges/dove.svg
rename to public/charges/dove.svg
diff --git a/charges/doveDisplayed.svg b/public/charges/doveDisplayed.svg
similarity index 100%
rename from charges/doveDisplayed.svg
rename to public/charges/doveDisplayed.svg
diff --git a/charges/dragonPassant.svg b/public/charges/dragonPassant.svg
similarity index 100%
rename from charges/dragonPassant.svg
rename to public/charges/dragonPassant.svg
diff --git a/charges/dragonRampant.svg b/public/charges/dragonRampant.svg
similarity index 100%
rename from charges/dragonRampant.svg
rename to public/charges/dragonRampant.svg
diff --git a/charges/dragonfly.svg b/public/charges/dragonfly.svg
similarity index 100%
rename from charges/dragonfly.svg
rename to public/charges/dragonfly.svg
diff --git a/charges/drakkar.svg b/public/charges/drakkar.svg
similarity index 100%
rename from charges/drakkar.svg
rename to public/charges/drakkar.svg
diff --git a/charges/drawingCompass.svg b/public/charges/drawingCompass.svg
similarity index 100%
rename from charges/drawingCompass.svg
rename to public/charges/drawingCompass.svg
diff --git a/charges/drum.svg b/public/charges/drum.svg
similarity index 100%
rename from charges/drum.svg
rename to public/charges/drum.svg
diff --git a/charges/duck.svg b/public/charges/duck.svg
similarity index 100%
rename from charges/duck.svg
rename to public/charges/duck.svg
diff --git a/charges/eagle.svg b/public/charges/eagle.svg
similarity index 100%
rename from charges/eagle.svg
rename to public/charges/eagle.svg
diff --git a/charges/eagleTwoHeads.svg b/public/charges/eagleTwoHeads.svg
similarity index 100%
rename from charges/eagleTwoHeads.svg
rename to public/charges/eagleTwoHeads.svg
diff --git a/charges/earOfWheat.svg b/public/charges/earOfWheat.svg
similarity index 100%
rename from charges/earOfWheat.svg
rename to public/charges/earOfWheat.svg
diff --git a/charges/elephant.svg b/public/charges/elephant.svg
similarity index 100%
rename from charges/elephant.svg
rename to public/charges/elephant.svg
diff --git a/charges/elephantHeadErased.svg b/public/charges/elephantHeadErased.svg
similarity index 100%
rename from charges/elephantHeadErased.svg
rename to public/charges/elephantHeadErased.svg
diff --git a/charges/escallop.svg b/public/charges/escallop.svg
similarity index 100%
rename from charges/escallop.svg
rename to public/charges/escallop.svg
diff --git a/charges/estoile.svg b/public/charges/estoile.svg
similarity index 100%
rename from charges/estoile.svg
rename to public/charges/estoile.svg
diff --git a/charges/falchion.svg b/public/charges/falchion.svg
similarity index 100%
rename from charges/falchion.svg
rename to public/charges/falchion.svg
diff --git a/charges/falcon.svg b/public/charges/falcon.svg
similarity index 100%
rename from charges/falcon.svg
rename to public/charges/falcon.svg
diff --git a/charges/fan.svg b/public/charges/fan.svg
similarity index 100%
rename from charges/fan.svg
rename to public/charges/fan.svg
diff --git a/charges/fasces.svg b/public/charges/fasces.svg
similarity index 100%
rename from charges/fasces.svg
rename to public/charges/fasces.svg
diff --git a/charges/feather.svg b/public/charges/feather.svg
similarity index 100%
rename from charges/feather.svg
rename to public/charges/feather.svg
diff --git a/charges/flamberge.svg b/public/charges/flamberge.svg
similarity index 100%
rename from charges/flamberge.svg
rename to public/charges/flamberge.svg
diff --git a/charges/flangedMace.svg b/public/charges/flangedMace.svg
similarity index 100%
rename from charges/flangedMace.svg
rename to public/charges/flangedMace.svg
diff --git a/charges/fleurDeLis.svg b/public/charges/fleurDeLis.svg
similarity index 100%
rename from charges/fleurDeLis.svg
rename to public/charges/fleurDeLis.svg
diff --git a/charges/fly.svg b/public/charges/fly.svg
similarity index 100%
rename from charges/fly.svg
rename to public/charges/fly.svg
diff --git a/charges/foot.svg b/public/charges/foot.svg
similarity index 100%
rename from charges/foot.svg
rename to public/charges/foot.svg
diff --git a/charges/fountain.svg b/public/charges/fountain.svg
similarity index 100%
rename from charges/fountain.svg
rename to public/charges/fountain.svg
diff --git a/charges/frog.svg b/public/charges/frog.svg
similarity index 100%
rename from charges/frog.svg
rename to public/charges/frog.svg
diff --git a/charges/fusil.svg b/public/charges/fusil.svg
similarity index 100%
rename from charges/fusil.svg
rename to public/charges/fusil.svg
diff --git a/charges/garb.svg b/public/charges/garb.svg
similarity index 100%
rename from charges/garb.svg
rename to public/charges/garb.svg
diff --git a/charges/gauntlet.svg b/public/charges/gauntlet.svg
similarity index 100%
rename from charges/gauntlet.svg
rename to public/charges/gauntlet.svg
diff --git a/charges/gear.svg b/public/charges/gear.svg
similarity index 100%
rename from charges/gear.svg
rename to public/charges/gear.svg
diff --git a/charges/goat.svg b/public/charges/goat.svg
similarity index 100%
rename from charges/goat.svg
rename to public/charges/goat.svg
diff --git a/charges/goutte.svg b/public/charges/goutte.svg
similarity index 100%
rename from charges/goutte.svg
rename to public/charges/goutte.svg
diff --git a/charges/grapeBunch.svg b/public/charges/grapeBunch.svg
similarity index 100%
rename from charges/grapeBunch.svg
rename to public/charges/grapeBunch.svg
diff --git a/charges/grapeBunch2.svg b/public/charges/grapeBunch2.svg
similarity index 100%
rename from charges/grapeBunch2.svg
rename to public/charges/grapeBunch2.svg
diff --git a/charges/grenade.svg b/public/charges/grenade.svg
similarity index 100%
rename from charges/grenade.svg
rename to public/charges/grenade.svg
diff --git a/charges/greyhoundCourant.svg b/public/charges/greyhoundCourant.svg
similarity index 100%
rename from charges/greyhoundCourant.svg
rename to public/charges/greyhoundCourant.svg
diff --git a/charges/greyhoundRampant.svg b/public/charges/greyhoundRampant.svg
similarity index 100%
rename from charges/greyhoundRampant.svg
rename to public/charges/greyhoundRampant.svg
diff --git a/charges/greyhoundSejant.svg b/public/charges/greyhoundSejant.svg
similarity index 100%
rename from charges/greyhoundSejant.svg
rename to public/charges/greyhoundSejant.svg
diff --git a/charges/griffinPassant.svg b/public/charges/griffinPassant.svg
similarity index 100%
rename from charges/griffinPassant.svg
rename to public/charges/griffinPassant.svg
diff --git a/charges/griffinRampant.svg b/public/charges/griffinRampant.svg
similarity index 100%
rename from charges/griffinRampant.svg
rename to public/charges/griffinRampant.svg
diff --git a/charges/hand.svg b/public/charges/hand.svg
similarity index 100%
rename from charges/hand.svg
rename to public/charges/hand.svg
diff --git a/charges/harp.svg b/public/charges/harp.svg
similarity index 100%
rename from charges/harp.svg
rename to public/charges/harp.svg
diff --git a/charges/hatchet.svg b/public/charges/hatchet.svg
similarity index 100%
rename from charges/hatchet.svg
rename to public/charges/hatchet.svg
diff --git a/charges/head.svg b/public/charges/head.svg
similarity index 100%
rename from charges/head.svg
rename to public/charges/head.svg
diff --git a/charges/headWreathed.svg b/public/charges/headWreathed.svg
similarity index 100%
rename from charges/headWreathed.svg
rename to public/charges/headWreathed.svg
diff --git a/charges/heart.svg b/public/charges/heart.svg
similarity index 100%
rename from charges/heart.svg
rename to public/charges/heart.svg
diff --git a/charges/hedgehog.svg b/public/charges/hedgehog.svg
similarity index 100%
rename from charges/hedgehog.svg
rename to public/charges/hedgehog.svg
diff --git a/charges/helmet.svg b/public/charges/helmet.svg
similarity index 100%
rename from charges/helmet.svg
rename to public/charges/helmet.svg
diff --git a/charges/helmetCorinthian.svg b/public/charges/helmetCorinthian.svg
similarity index 100%
rename from charges/helmetCorinthian.svg
rename to public/charges/helmetCorinthian.svg
diff --git a/charges/helmetGreat.svg b/public/charges/helmetGreat.svg
similarity index 100%
rename from charges/helmetGreat.svg
rename to public/charges/helmetGreat.svg
diff --git a/charges/helmetZischagge.svg b/public/charges/helmetZischagge.svg
similarity index 100%
rename from charges/helmetZischagge.svg
rename to public/charges/helmetZischagge.svg
diff --git a/charges/heron.svg b/public/charges/heron.svg
similarity index 100%
rename from charges/heron.svg
rename to public/charges/heron.svg
diff --git a/charges/hindStatant.svg b/public/charges/hindStatant.svg
similarity index 100%
rename from charges/hindStatant.svg
rename to public/charges/hindStatant.svg
diff --git a/charges/hook.svg b/public/charges/hook.svg
similarity index 100%
rename from charges/hook.svg
rename to public/charges/hook.svg
diff --git a/charges/horseHeadCouped.svg b/public/charges/horseHeadCouped.svg
similarity index 100%
rename from charges/horseHeadCouped.svg
rename to public/charges/horseHeadCouped.svg
diff --git a/charges/horsePassant.svg b/public/charges/horsePassant.svg
similarity index 100%
rename from charges/horsePassant.svg
rename to public/charges/horsePassant.svg
diff --git a/charges/horseRampant.svg b/public/charges/horseRampant.svg
similarity index 100%
rename from charges/horseRampant.svg
rename to public/charges/horseRampant.svg
diff --git a/charges/horseSalient.svg b/public/charges/horseSalient.svg
similarity index 100%
rename from charges/horseSalient.svg
rename to public/charges/horseSalient.svg
diff --git a/charges/horseshoe.svg b/public/charges/horseshoe.svg
similarity index 100%
rename from charges/horseshoe.svg
rename to public/charges/horseshoe.svg
diff --git a/charges/hourglass.svg b/public/charges/hourglass.svg
similarity index 100%
rename from charges/hourglass.svg
rename to public/charges/hourglass.svg
diff --git a/charges/key.svg b/public/charges/key.svg
similarity index 100%
rename from charges/key.svg
rename to public/charges/key.svg
diff --git a/charges/ladder.svg b/public/charges/ladder.svg
similarity index 100%
rename from charges/ladder.svg
rename to public/charges/ladder.svg
diff --git a/charges/ladder2.svg b/public/charges/ladder2.svg
similarity index 100%
rename from charges/ladder2.svg
rename to public/charges/ladder2.svg
diff --git a/charges/ladybird.svg b/public/charges/ladybird.svg
similarity index 100%
rename from charges/ladybird.svg
rename to public/charges/ladybird.svg
diff --git a/charges/lamb.svg b/public/charges/lamb.svg
similarity index 100%
rename from charges/lamb.svg
rename to public/charges/lamb.svg
diff --git a/charges/lambPassantReguardant.svg b/public/charges/lambPassantReguardant.svg
similarity index 100%
rename from charges/lambPassantReguardant.svg
rename to public/charges/lambPassantReguardant.svg
diff --git a/charges/lanceHead.svg b/public/charges/lanceHead.svg
similarity index 100%
rename from charges/lanceHead.svg
rename to public/charges/lanceHead.svg
diff --git a/charges/lanceWithBanner.svg b/public/charges/lanceWithBanner.svg
similarity index 100%
rename from charges/lanceWithBanner.svg
rename to public/charges/lanceWithBanner.svg
diff --git a/charges/laurelWreath.svg b/public/charges/laurelWreath.svg
similarity index 100%
rename from charges/laurelWreath.svg
rename to public/charges/laurelWreath.svg
diff --git a/charges/laurelWreath2.svg b/public/charges/laurelWreath2.svg
similarity index 100%
rename from charges/laurelWreath2.svg
rename to public/charges/laurelWreath2.svg
diff --git a/charges/lighthouse.svg b/public/charges/lighthouse.svg
similarity index 100%
rename from charges/lighthouse.svg
rename to public/charges/lighthouse.svg
diff --git a/charges/lionHeadCaboshed.svg b/public/charges/lionHeadCaboshed.svg
similarity index 100%
rename from charges/lionHeadCaboshed.svg
rename to public/charges/lionHeadCaboshed.svg
diff --git a/charges/lionHeadErased.svg b/public/charges/lionHeadErased.svg
similarity index 100%
rename from charges/lionHeadErased.svg
rename to public/charges/lionHeadErased.svg
diff --git a/charges/lionPassant.svg b/public/charges/lionPassant.svg
similarity index 100%
rename from charges/lionPassant.svg
rename to public/charges/lionPassant.svg
diff --git a/charges/lionPassantGuardant.svg b/public/charges/lionPassantGuardant.svg
similarity index 100%
rename from charges/lionPassantGuardant.svg
rename to public/charges/lionPassantGuardant.svg
diff --git a/charges/lionRampant.svg b/public/charges/lionRampant.svg
similarity index 100%
rename from charges/lionRampant.svg
rename to public/charges/lionRampant.svg
diff --git a/charges/lionSejant.svg b/public/charges/lionSejant.svg
similarity index 100%
rename from charges/lionSejant.svg
rename to public/charges/lionSejant.svg
diff --git a/charges/lizard.svg b/public/charges/lizard.svg
similarity index 100%
rename from charges/lizard.svg
rename to public/charges/lizard.svg
diff --git a/charges/lochaberAxe.svg b/public/charges/lochaberAxe.svg
similarity index 100%
rename from charges/lochaberAxe.svg
rename to public/charges/lochaberAxe.svg
diff --git a/charges/log.svg b/public/charges/log.svg
similarity index 100%
rename from charges/log.svg
rename to public/charges/log.svg
diff --git a/charges/lozenge.svg b/public/charges/lozenge.svg
similarity index 100%
rename from charges/lozenge.svg
rename to public/charges/lozenge.svg
diff --git a/charges/lozengeFaceted.svg b/public/charges/lozengeFaceted.svg
similarity index 100%
rename from charges/lozengeFaceted.svg
rename to public/charges/lozengeFaceted.svg
diff --git a/charges/lozengePloye.svg b/public/charges/lozengePloye.svg
similarity index 100%
rename from charges/lozengePloye.svg
rename to public/charges/lozengePloye.svg
diff --git a/charges/lute.svg b/public/charges/lute.svg
similarity index 100%
rename from charges/lute.svg
rename to public/charges/lute.svg
diff --git a/charges/lymphad.svg b/public/charges/lymphad.svg
similarity index 100%
rename from charges/lymphad.svg
rename to public/charges/lymphad.svg
diff --git a/charges/lyre.svg b/public/charges/lyre.svg
similarity index 100%
rename from charges/lyre.svg
rename to public/charges/lyre.svg
diff --git a/charges/mace.svg b/public/charges/mace.svg
similarity index 100%
rename from charges/mace.svg
rename to public/charges/mace.svg
diff --git a/charges/maces.svg b/public/charges/maces.svg
similarity index 100%
rename from charges/maces.svg
rename to public/charges/maces.svg
diff --git a/charges/mallet.svg b/public/charges/mallet.svg
similarity index 100%
rename from charges/mallet.svg
rename to public/charges/mallet.svg
diff --git a/charges/mantle.svg b/public/charges/mantle.svg
similarity index 100%
rename from charges/mantle.svg
rename to public/charges/mantle.svg
diff --git a/charges/mapleLeaf.svg b/public/charges/mapleLeaf.svg
similarity index 100%
rename from charges/mapleLeaf.svg
rename to public/charges/mapleLeaf.svg
diff --git a/charges/martenCourant.svg b/public/charges/martenCourant.svg
similarity index 100%
rename from charges/martenCourant.svg
rename to public/charges/martenCourant.svg
diff --git a/charges/mascle.svg b/public/charges/mascle.svg
similarity index 100%
rename from charges/mascle.svg
rename to public/charges/mascle.svg
diff --git a/charges/mastiffStatant.svg b/public/charges/mastiffStatant.svg
similarity index 100%
rename from charges/mastiffStatant.svg
rename to public/charges/mastiffStatant.svg
diff --git a/charges/millstone.svg b/public/charges/millstone.svg
similarity index 100%
rename from charges/millstone.svg
rename to public/charges/millstone.svg
diff --git a/charges/mitre.svg b/public/charges/mitre.svg
similarity index 100%
rename from charges/mitre.svg
rename to public/charges/mitre.svg
diff --git a/charges/monk.svg b/public/charges/monk.svg
similarity index 100%
rename from charges/monk.svg
rename to public/charges/monk.svg
diff --git a/charges/moonInCrescent.svg b/public/charges/moonInCrescent.svg
similarity index 100%
rename from charges/moonInCrescent.svg
rename to public/charges/moonInCrescent.svg
diff --git a/charges/mullet.svg b/public/charges/mullet.svg
similarity index 100%
rename from charges/mullet.svg
rename to public/charges/mullet.svg
diff --git a/charges/mullet10.svg b/public/charges/mullet10.svg
similarity index 100%
rename from charges/mullet10.svg
rename to public/charges/mullet10.svg
diff --git a/charges/mullet4.svg b/public/charges/mullet4.svg
similarity index 100%
rename from charges/mullet4.svg
rename to public/charges/mullet4.svg
diff --git a/charges/mullet6.svg b/public/charges/mullet6.svg
similarity index 100%
rename from charges/mullet6.svg
rename to public/charges/mullet6.svg
diff --git a/charges/mullet6Faceted.svg b/public/charges/mullet6Faceted.svg
similarity index 100%
rename from charges/mullet6Faceted.svg
rename to public/charges/mullet6Faceted.svg
diff --git a/charges/mullet6Pierced.svg b/public/charges/mullet6Pierced.svg
similarity index 100%
rename from charges/mullet6Pierced.svg
rename to public/charges/mullet6Pierced.svg
diff --git a/charges/mullet7.svg b/public/charges/mullet7.svg
similarity index 100%
rename from charges/mullet7.svg
rename to public/charges/mullet7.svg
diff --git a/charges/mullet8.svg b/public/charges/mullet8.svg
similarity index 100%
rename from charges/mullet8.svg
rename to public/charges/mullet8.svg
diff --git a/charges/mulletFaceted.svg b/public/charges/mulletFaceted.svg
similarity index 100%
rename from charges/mulletFaceted.svg
rename to public/charges/mulletFaceted.svg
diff --git a/charges/mulletPierced.svg b/public/charges/mulletPierced.svg
similarity index 100%
rename from charges/mulletPierced.svg
rename to public/charges/mulletPierced.svg
diff --git a/charges/oak.svg b/public/charges/oak.svg
similarity index 100%
rename from charges/oak.svg
rename to public/charges/oak.svg
diff --git a/charges/orb.svg b/public/charges/orb.svg
similarity index 100%
rename from charges/orb.svg
rename to public/charges/orb.svg
diff --git a/charges/ouroboros.svg b/public/charges/ouroboros.svg
similarity index 100%
rename from charges/ouroboros.svg
rename to public/charges/ouroboros.svg
diff --git a/charges/owl.svg b/public/charges/owl.svg
similarity index 100%
rename from charges/owl.svg
rename to public/charges/owl.svg
diff --git a/charges/owlDisplayed.svg b/public/charges/owlDisplayed.svg
similarity index 100%
rename from charges/owlDisplayed.svg
rename to public/charges/owlDisplayed.svg
diff --git a/charges/palace.svg b/public/charges/palace.svg
similarity index 100%
rename from charges/palace.svg
rename to public/charges/palace.svg
diff --git a/charges/palmTree.svg b/public/charges/palmTree.svg
similarity index 100%
rename from charges/palmTree.svg
rename to public/charges/palmTree.svg
diff --git a/charges/parrot.svg b/public/charges/parrot.svg
similarity index 100%
rename from charges/parrot.svg
rename to public/charges/parrot.svg
diff --git a/charges/peacock.svg b/public/charges/peacock.svg
similarity index 100%
rename from charges/peacock.svg
rename to public/charges/peacock.svg
diff --git a/charges/peacockInPride.svg b/public/charges/peacockInPride.svg
similarity index 100%
rename from charges/peacockInPride.svg
rename to public/charges/peacockInPride.svg
diff --git a/charges/pear.svg b/public/charges/pear.svg
similarity index 100%
rename from charges/pear.svg
rename to public/charges/pear.svg
diff --git a/charges/pegasus.svg b/public/charges/pegasus.svg
similarity index 100%
rename from charges/pegasus.svg
rename to public/charges/pegasus.svg
diff --git a/charges/pike.svg b/public/charges/pike.svg
similarity index 100%
rename from charges/pike.svg
rename to public/charges/pike.svg
diff --git a/charges/pillar.svg b/public/charges/pillar.svg
similarity index 100%
rename from charges/pillar.svg
rename to public/charges/pillar.svg
diff --git a/charges/pincers.svg b/public/charges/pincers.svg
similarity index 100%
rename from charges/pincers.svg
rename to public/charges/pincers.svg
diff --git a/charges/pineCone.svg b/public/charges/pineCone.svg
similarity index 100%
rename from charges/pineCone.svg
rename to public/charges/pineCone.svg
diff --git a/charges/pineTree.svg b/public/charges/pineTree.svg
similarity index 100%
rename from charges/pineTree.svg
rename to public/charges/pineTree.svg
diff --git a/charges/pique.svg b/public/charges/pique.svg
similarity index 100%
rename from charges/pique.svg
rename to public/charges/pique.svg
diff --git a/charges/plaice.svg b/public/charges/plaice.svg
similarity index 100%
rename from charges/plaice.svg
rename to public/charges/plaice.svg
diff --git a/charges/plough.svg b/public/charges/plough.svg
similarity index 100%
rename from charges/plough.svg
rename to public/charges/plough.svg
diff --git a/charges/ploughshare.svg b/public/charges/ploughshare.svg
similarity index 100%
rename from charges/ploughshare.svg
rename to public/charges/ploughshare.svg
diff --git a/charges/porcupine.svg b/public/charges/porcupine.svg
similarity index 100%
rename from charges/porcupine.svg
rename to public/charges/porcupine.svg
diff --git a/charges/portcullis.svg b/public/charges/portcullis.svg
similarity index 100%
rename from charges/portcullis.svg
rename to public/charges/portcullis.svg
diff --git a/charges/pot.svg b/public/charges/pot.svg
similarity index 100%
rename from charges/pot.svg
rename to public/charges/pot.svg
diff --git a/charges/quatrefoil.svg b/public/charges/quatrefoil.svg
similarity index 100%
rename from charges/quatrefoil.svg
rename to public/charges/quatrefoil.svg
diff --git a/charges/rabbitSejant.svg b/public/charges/rabbitSejant.svg
similarity index 100%
rename from charges/rabbitSejant.svg
rename to public/charges/rabbitSejant.svg
diff --git a/charges/raft.svg b/public/charges/raft.svg
similarity index 100%
rename from charges/raft.svg
rename to public/charges/raft.svg
diff --git a/charges/rake.svg b/public/charges/rake.svg
similarity index 100%
rename from charges/rake.svg
rename to public/charges/rake.svg
diff --git a/charges/ramHeadErased.svg b/public/charges/ramHeadErased.svg
similarity index 100%
rename from charges/ramHeadErased.svg
rename to public/charges/ramHeadErased.svg
diff --git a/charges/ramPassant.svg b/public/charges/ramPassant.svg
similarity index 100%
rename from charges/ramPassant.svg
rename to public/charges/ramPassant.svg
diff --git a/charges/ramsHorn.svg b/public/charges/ramsHorn.svg
similarity index 100%
rename from charges/ramsHorn.svg
rename to public/charges/ramsHorn.svg
diff --git a/charges/rapier.svg b/public/charges/rapier.svg
similarity index 100%
rename from charges/rapier.svg
rename to public/charges/rapier.svg
diff --git a/charges/ratRampant.svg b/public/charges/ratRampant.svg
similarity index 100%
rename from charges/ratRampant.svg
rename to public/charges/ratRampant.svg
diff --git a/charges/raven.svg b/public/charges/raven.svg
similarity index 100%
rename from charges/raven.svg
rename to public/charges/raven.svg
diff --git a/charges/rhinoceros.svg b/public/charges/rhinoceros.svg
similarity index 100%
rename from charges/rhinoceros.svg
rename to public/charges/rhinoceros.svg
diff --git a/charges/ribbon1.svg b/public/charges/ribbon1.svg
similarity index 100%
rename from charges/ribbon1.svg
rename to public/charges/ribbon1.svg
diff --git a/charges/ribbon2.svg b/public/charges/ribbon2.svg
similarity index 100%
rename from charges/ribbon2.svg
rename to public/charges/ribbon2.svg
diff --git a/charges/ribbon3.svg b/public/charges/ribbon3.svg
similarity index 100%
rename from charges/ribbon3.svg
rename to public/charges/ribbon3.svg
diff --git a/charges/ribbon4.svg b/public/charges/ribbon4.svg
similarity index 100%
rename from charges/ribbon4.svg
rename to public/charges/ribbon4.svg
diff --git a/charges/ribbon5.svg b/public/charges/ribbon5.svg
similarity index 100%
rename from charges/ribbon5.svg
rename to public/charges/ribbon5.svg
diff --git a/charges/ribbon6.svg b/public/charges/ribbon6.svg
similarity index 100%
rename from charges/ribbon6.svg
rename to public/charges/ribbon6.svg
diff --git a/charges/ribbon7.svg b/public/charges/ribbon7.svg
similarity index 100%
rename from charges/ribbon7.svg
rename to public/charges/ribbon7.svg
diff --git a/charges/ribbon8.svg b/public/charges/ribbon8.svg
similarity index 100%
rename from charges/ribbon8.svg
rename to public/charges/ribbon8.svg
diff --git a/charges/rose.svg b/public/charges/rose.svg
similarity index 100%
rename from charges/rose.svg
rename to public/charges/rose.svg
diff --git a/charges/roundel.svg b/public/charges/roundel.svg
similarity index 100%
rename from charges/roundel.svg
rename to public/charges/roundel.svg
diff --git a/charges/roundel2.svg b/public/charges/roundel2.svg
similarity index 100%
rename from charges/roundel2.svg
rename to public/charges/roundel2.svg
diff --git a/charges/rustre.svg b/public/charges/rustre.svg
similarity index 100%
rename from charges/rustre.svg
rename to public/charges/rustre.svg
diff --git a/charges/sabre.svg b/public/charges/sabre.svg
similarity index 100%
rename from charges/sabre.svg
rename to public/charges/sabre.svg
diff --git a/charges/sabre2.svg b/public/charges/sabre2.svg
similarity index 100%
rename from charges/sabre2.svg
rename to public/charges/sabre2.svg
diff --git a/charges/sabresCrossed.svg b/public/charges/sabresCrossed.svg
similarity index 100%
rename from charges/sabresCrossed.svg
rename to public/charges/sabresCrossed.svg
diff --git a/charges/sagittarius.svg b/public/charges/sagittarius.svg
similarity index 100%
rename from charges/sagittarius.svg
rename to public/charges/sagittarius.svg
diff --git a/charges/salmon.svg b/public/charges/salmon.svg
similarity index 100%
rename from charges/salmon.svg
rename to public/charges/salmon.svg
diff --git a/charges/saw.svg b/public/charges/saw.svg
similarity index 100%
rename from charges/saw.svg
rename to public/charges/saw.svg
diff --git a/charges/scale.svg b/public/charges/scale.svg
similarity index 100%
rename from charges/scale.svg
rename to public/charges/scale.svg
diff --git a/charges/scaleImbalanced.svg b/public/charges/scaleImbalanced.svg
similarity index 100%
rename from charges/scaleImbalanced.svg
rename to public/charges/scaleImbalanced.svg
diff --git a/charges/scalesHanging.svg b/public/charges/scalesHanging.svg
similarity index 100%
rename from charges/scalesHanging.svg
rename to public/charges/scalesHanging.svg
diff --git a/charges/sceptre.svg b/public/charges/sceptre.svg
similarity index 100%
rename from charges/sceptre.svg
rename to public/charges/sceptre.svg
diff --git a/charges/scissors.svg b/public/charges/scissors.svg
similarity index 100%
rename from charges/scissors.svg
rename to public/charges/scissors.svg
diff --git a/charges/scissors2.svg b/public/charges/scissors2.svg
similarity index 100%
rename from charges/scissors2.svg
rename to public/charges/scissors2.svg
diff --git a/charges/scorpion.svg b/public/charges/scorpion.svg
similarity index 100%
rename from charges/scorpion.svg
rename to public/charges/scorpion.svg
diff --git a/charges/scrollClosed.svg b/public/charges/scrollClosed.svg
similarity index 100%
rename from charges/scrollClosed.svg
rename to public/charges/scrollClosed.svg
diff --git a/charges/scythe.svg b/public/charges/scythe.svg
similarity index 100%
rename from charges/scythe.svg
rename to public/charges/scythe.svg
diff --git a/charges/scythe2.svg b/public/charges/scythe2.svg
similarity index 100%
rename from charges/scythe2.svg
rename to public/charges/scythe2.svg
diff --git a/charges/serpent.svg b/public/charges/serpent.svg
similarity index 100%
rename from charges/serpent.svg
rename to public/charges/serpent.svg
diff --git a/charges/sextifoil.svg b/public/charges/sextifoil.svg
similarity index 100%
rename from charges/sextifoil.svg
rename to public/charges/sextifoil.svg
diff --git a/charges/shears.svg b/public/charges/shears.svg
similarity index 100%
rename from charges/shears.svg
rename to public/charges/shears.svg
diff --git a/charges/shield.svg b/public/charges/shield.svg
similarity index 100%
rename from charges/shield.svg
rename to public/charges/shield.svg
diff --git a/charges/shipWheel.svg b/public/charges/shipWheel.svg
similarity index 100%
rename from charges/shipWheel.svg
rename to public/charges/shipWheel.svg
diff --git a/charges/sickle.svg b/public/charges/sickle.svg
similarity index 100%
rename from charges/sickle.svg
rename to public/charges/sickle.svg
diff --git a/charges/skeleton.svg b/public/charges/skeleton.svg
similarity index 100%
rename from charges/skeleton.svg
rename to public/charges/skeleton.svg
diff --git a/charges/skull.svg b/public/charges/skull.svg
similarity index 100%
rename from charges/skull.svg
rename to public/charges/skull.svg
diff --git a/charges/skull2.svg b/public/charges/skull2.svg
similarity index 100%
rename from charges/skull2.svg
rename to public/charges/skull2.svg
diff --git a/charges/snail.svg b/public/charges/snail.svg
similarity index 100%
rename from charges/snail.svg
rename to public/charges/snail.svg
diff --git a/charges/snake.svg b/public/charges/snake.svg
similarity index 100%
rename from charges/snake.svg
rename to public/charges/snake.svg
diff --git a/charges/snowflake.svg b/public/charges/snowflake.svg
similarity index 100%
rename from charges/snowflake.svg
rename to public/charges/snowflake.svg
diff --git a/charges/spear.svg b/public/charges/spear.svg
similarity index 100%
rename from charges/spear.svg
rename to public/charges/spear.svg
diff --git a/charges/spiral.svg b/public/charges/spiral.svg
similarity index 100%
rename from charges/spiral.svg
rename to public/charges/spiral.svg
diff --git a/charges/squirrel.svg b/public/charges/squirrel.svg
similarity index 100%
rename from charges/squirrel.svg
rename to public/charges/squirrel.svg
diff --git a/charges/stagLodgedRegardant.svg b/public/charges/stagLodgedRegardant.svg
similarity index 100%
rename from charges/stagLodgedRegardant.svg
rename to public/charges/stagLodgedRegardant.svg
diff --git a/charges/stagPassant.svg b/public/charges/stagPassant.svg
similarity index 100%
rename from charges/stagPassant.svg
rename to public/charges/stagPassant.svg
diff --git a/charges/stagsAttires.svg b/public/charges/stagsAttires.svg
similarity index 100%
rename from charges/stagsAttires.svg
rename to public/charges/stagsAttires.svg
diff --git a/charges/stirrup.svg b/public/charges/stirrup.svg
similarity index 100%
rename from charges/stirrup.svg
rename to public/charges/stirrup.svg
diff --git a/charges/sun.svg b/public/charges/sun.svg
similarity index 100%
rename from charges/sun.svg
rename to public/charges/sun.svg
diff --git a/charges/sunInSplendour.svg b/public/charges/sunInSplendour.svg
similarity index 100%
rename from charges/sunInSplendour.svg
rename to public/charges/sunInSplendour.svg
diff --git a/charges/sunInSplendour2.svg b/public/charges/sunInSplendour2.svg
similarity index 100%
rename from charges/sunInSplendour2.svg
rename to public/charges/sunInSplendour2.svg
diff --git a/charges/swallow.svg b/public/charges/swallow.svg
similarity index 100%
rename from charges/swallow.svg
rename to public/charges/swallow.svg
diff --git a/charges/swan.svg b/public/charges/swan.svg
similarity index 100%
rename from charges/swan.svg
rename to public/charges/swan.svg
diff --git a/charges/swanErased.svg b/public/charges/swanErased.svg
similarity index 100%
rename from charges/swanErased.svg
rename to public/charges/swanErased.svg
diff --git a/charges/sword.svg b/public/charges/sword.svg
similarity index 100%
rename from charges/sword.svg
rename to public/charges/sword.svg
diff --git a/charges/talbotPassant.svg b/public/charges/talbotPassant.svg
similarity index 100%
rename from charges/talbotPassant.svg
rename to public/charges/talbotPassant.svg
diff --git a/charges/talbotSejant.svg b/public/charges/talbotSejant.svg
similarity index 100%
rename from charges/talbotSejant.svg
rename to public/charges/talbotSejant.svg
diff --git a/charges/template.svg b/public/charges/template.svg
similarity index 100%
rename from charges/template.svg
rename to public/charges/template.svg
diff --git a/charges/thistle.svg b/public/charges/thistle.svg
similarity index 100%
rename from charges/thistle.svg
rename to public/charges/thistle.svg
diff --git a/charges/tower.svg b/public/charges/tower.svg
similarity index 100%
rename from charges/tower.svg
rename to public/charges/tower.svg
diff --git a/charges/tree.svg b/public/charges/tree.svg
similarity index 100%
rename from charges/tree.svg
rename to public/charges/tree.svg
diff --git a/charges/trefle.svg b/public/charges/trefle.svg
similarity index 100%
rename from charges/trefle.svg
rename to public/charges/trefle.svg
diff --git a/charges/trefoil.svg b/public/charges/trefoil.svg
similarity index 100%
rename from charges/trefoil.svg
rename to public/charges/trefoil.svg
diff --git a/charges/triangle.svg b/public/charges/triangle.svg
similarity index 100%
rename from charges/triangle.svg
rename to public/charges/triangle.svg
diff --git a/charges/trianglePierced.svg b/public/charges/trianglePierced.svg
similarity index 100%
rename from charges/trianglePierced.svg
rename to public/charges/trianglePierced.svg
diff --git a/charges/trowel.svg b/public/charges/trowel.svg
similarity index 100%
rename from charges/trowel.svg
rename to public/charges/trowel.svg
diff --git a/charges/unicornRampant.svg b/public/charges/unicornRampant.svg
similarity index 100%
rename from charges/unicornRampant.svg
rename to public/charges/unicornRampant.svg
diff --git a/charges/wasp.svg b/public/charges/wasp.svg
similarity index 100%
rename from charges/wasp.svg
rename to public/charges/wasp.svg
diff --git a/charges/wheatStalk.svg b/public/charges/wheatStalk.svg
similarity index 100%
rename from charges/wheatStalk.svg
rename to public/charges/wheatStalk.svg
diff --git a/charges/wheel.svg b/public/charges/wheel.svg
similarity index 100%
rename from charges/wheel.svg
rename to public/charges/wheel.svg
diff --git a/charges/windmill.svg b/public/charges/windmill.svg
similarity index 100%
rename from charges/windmill.svg
rename to public/charges/windmill.svg
diff --git a/charges/wing.svg b/public/charges/wing.svg
similarity index 100%
rename from charges/wing.svg
rename to public/charges/wing.svg
diff --git a/charges/wingSword.svg b/public/charges/wingSword.svg
similarity index 100%
rename from charges/wingSword.svg
rename to public/charges/wingSword.svg
diff --git a/charges/wolfHeadErased.svg b/public/charges/wolfHeadErased.svg
similarity index 100%
rename from charges/wolfHeadErased.svg
rename to public/charges/wolfHeadErased.svg
diff --git a/charges/wolfPassant.svg b/public/charges/wolfPassant.svg
similarity index 100%
rename from charges/wolfPassant.svg
rename to public/charges/wolfPassant.svg
diff --git a/charges/wolfRampant.svg b/public/charges/wolfRampant.svg
similarity index 100%
rename from charges/wolfRampant.svg
rename to public/charges/wolfRampant.svg
diff --git a/charges/wolfStatant.svg b/public/charges/wolfStatant.svg
similarity index 100%
rename from charges/wolfStatant.svg
rename to public/charges/wolfStatant.svg
diff --git a/charges/wyvern.svg b/public/charges/wyvern.svg
similarity index 100%
rename from charges/wyvern.svg
rename to public/charges/wyvern.svg
diff --git a/charges/wyvernWithWingsDisplayed.svg b/public/charges/wyvernWithWingsDisplayed.svg
similarity index 100%
rename from charges/wyvernWithWingsDisplayed.svg
rename to public/charges/wyvernWithWingsDisplayed.svg
diff --git a/components/fill-box.js b/public/components/fill-box.js
similarity index 100%
rename from components/fill-box.js
rename to public/components/fill-box.js
diff --git a/components/slider-input.js b/public/components/slider-input.js
similarity index 100%
rename from components/slider-input.js
rename to public/components/slider-input.js
diff --git a/config/heightmap-templates.js b/public/config/heightmap-templates.js
similarity index 100%
rename from config/heightmap-templates.js
rename to public/config/heightmap-templates.js
diff --git a/config/precreated-heightmaps.js b/public/config/precreated-heightmaps.js
similarity index 100%
rename from config/precreated-heightmaps.js
rename to public/config/precreated-heightmaps.js
diff --git a/dropbox.html b/public/dropbox.html
similarity index 100%
rename from dropbox.html
rename to public/dropbox.html
diff --git a/heightmaps/africa-centric.png b/public/heightmaps/africa-centric.png
similarity index 100%
rename from heightmaps/africa-centric.png
rename to public/heightmaps/africa-centric.png
diff --git a/heightmaps/arabia.png b/public/heightmaps/arabia.png
similarity index 100%
rename from heightmaps/arabia.png
rename to public/heightmaps/arabia.png
diff --git a/heightmaps/atlantics.png b/public/heightmaps/atlantics.png
similarity index 100%
rename from heightmaps/atlantics.png
rename to public/heightmaps/atlantics.png
diff --git a/heightmaps/britain.png b/public/heightmaps/britain.png
similarity index 100%
rename from heightmaps/britain.png
rename to public/heightmaps/britain.png
diff --git a/heightmaps/caribbean.png b/public/heightmaps/caribbean.png
similarity index 100%
rename from heightmaps/caribbean.png
rename to public/heightmaps/caribbean.png
diff --git a/heightmaps/east-asia.png b/public/heightmaps/east-asia.png
similarity index 100%
rename from heightmaps/east-asia.png
rename to public/heightmaps/east-asia.png
diff --git a/heightmaps/eurasia.png b/public/heightmaps/eurasia.png
similarity index 100%
rename from heightmaps/eurasia.png
rename to public/heightmaps/eurasia.png
diff --git a/heightmaps/europe-accented.png b/public/heightmaps/europe-accented.png
similarity index 100%
rename from heightmaps/europe-accented.png
rename to public/heightmaps/europe-accented.png
diff --git a/heightmaps/europe-and-central-asia.png b/public/heightmaps/europe-and-central-asia.png
similarity index 100%
rename from heightmaps/europe-and-central-asia.png
rename to public/heightmaps/europe-and-central-asia.png
diff --git a/heightmaps/europe-central.png b/public/heightmaps/europe-central.png
similarity index 100%
rename from heightmaps/europe-central.png
rename to public/heightmaps/europe-central.png
diff --git a/heightmaps/europe-north.png b/public/heightmaps/europe-north.png
similarity index 100%
rename from heightmaps/europe-north.png
rename to public/heightmaps/europe-north.png
diff --git a/heightmaps/europe.png b/public/heightmaps/europe.png
similarity index 100%
rename from heightmaps/europe.png
rename to public/heightmaps/europe.png
diff --git a/heightmaps/greenland.png b/public/heightmaps/greenland.png
similarity index 100%
rename from heightmaps/greenland.png
rename to public/heightmaps/greenland.png
diff --git a/heightmaps/hellenica.png b/public/heightmaps/hellenica.png
similarity index 100%
rename from heightmaps/hellenica.png
rename to public/heightmaps/hellenica.png
diff --git a/heightmaps/iceland.png b/public/heightmaps/iceland.png
similarity index 100%
rename from heightmaps/iceland.png
rename to public/heightmaps/iceland.png
diff --git a/heightmaps/import-rules.txt b/public/heightmaps/import-rules.txt
similarity index 100%
rename from heightmaps/import-rules.txt
rename to public/heightmaps/import-rules.txt
diff --git a/heightmaps/indian-ocean.png b/public/heightmaps/indian-ocean.png
similarity index 100%
rename from heightmaps/indian-ocean.png
rename to public/heightmaps/indian-ocean.png
diff --git a/heightmaps/mediterranean-sea.png b/public/heightmaps/mediterranean-sea.png
similarity index 100%
rename from heightmaps/mediterranean-sea.png
rename to public/heightmaps/mediterranean-sea.png
diff --git a/heightmaps/middle-east.png b/public/heightmaps/middle-east.png
similarity index 100%
rename from heightmaps/middle-east.png
rename to public/heightmaps/middle-east.png
diff --git a/heightmaps/north-america.png b/public/heightmaps/north-america.png
similarity index 100%
rename from heightmaps/north-america.png
rename to public/heightmaps/north-america.png
diff --git a/heightmaps/us-centric.png b/public/heightmaps/us-centric.png
similarity index 100%
rename from heightmaps/us-centric.png
rename to public/heightmaps/us-centric.png
diff --git a/heightmaps/us-mainland.png b/public/heightmaps/us-mainland.png
similarity index 100%
rename from heightmaps/us-mainland.png
rename to public/heightmaps/us-mainland.png
diff --git a/heightmaps/world-from-pacific.png b/public/heightmaps/world-from-pacific.png
similarity index 100%
rename from heightmaps/world-from-pacific.png
rename to public/heightmaps/world-from-pacific.png
diff --git a/heightmaps/world.png b/public/heightmaps/world.png
similarity index 100%
rename from heightmaps/world.png
rename to public/heightmaps/world.png
diff --git a/icons.css b/public/icons.css
similarity index 100%
rename from icons.css
rename to public/icons.css
diff --git a/images/Discord.png b/public/images/Discord.png
similarity index 100%
rename from images/Discord.png
rename to public/images/Discord.png
diff --git a/images/Facebook.png b/public/images/Facebook.png
similarity index 100%
rename from images/Facebook.png
rename to public/images/Facebook.png
diff --git a/images/Pinterest.png b/public/images/Pinterest.png
similarity index 100%
rename from images/Pinterest.png
rename to public/images/Pinterest.png
diff --git a/images/Reddit.png b/public/images/Reddit.png
similarity index 100%
rename from images/Reddit.png
rename to public/images/Reddit.png
diff --git a/images/Twitter.png b/public/images/Twitter.png
similarity index 100%
rename from images/Twitter.png
rename to public/images/Twitter.png
diff --git a/images/icons/favicon-16x16.png b/public/images/icons/favicon-16x16.png
similarity index 100%
rename from images/icons/favicon-16x16.png
rename to public/images/icons/favicon-16x16.png
diff --git a/images/icons/favicon-32x32.png b/public/images/icons/favicon-32x32.png
similarity index 100%
rename from images/icons/favicon-32x32.png
rename to public/images/icons/favicon-32x32.png
diff --git a/images/icons/icon_x512.png b/public/images/icons/icon_x512.png
similarity index 100%
rename from images/icons/icon_x512.png
rename to public/images/icons/icon_x512.png
diff --git a/images/icons/maskable_icon_x128.png b/public/images/icons/maskable_icon_x128.png
similarity index 100%
rename from images/icons/maskable_icon_x128.png
rename to public/images/icons/maskable_icon_x128.png
diff --git a/images/icons/maskable_icon_x192.png b/public/images/icons/maskable_icon_x192.png
similarity index 100%
rename from images/icons/maskable_icon_x192.png
rename to public/images/icons/maskable_icon_x192.png
diff --git a/images/icons/maskable_icon_x384.png b/public/images/icons/maskable_icon_x384.png
similarity index 100%
rename from images/icons/maskable_icon_x384.png
rename to public/images/icons/maskable_icon_x384.png
diff --git a/images/icons/maskable_icon_x512.png b/public/images/icons/maskable_icon_x512.png
similarity index 100%
rename from images/icons/maskable_icon_x512.png
rename to public/images/icons/maskable_icon_x512.png
diff --git a/images/kiwiroo.png b/public/images/kiwiroo.png
similarity index 100%
rename from images/kiwiroo.png
rename to public/images/kiwiroo.png
diff --git a/images/pattern1.png b/public/images/pattern1.png
similarity index 100%
rename from images/pattern1.png
rename to public/images/pattern1.png
diff --git a/images/pattern2.png b/public/images/pattern2.png
similarity index 100%
rename from images/pattern2.png
rename to public/images/pattern2.png
diff --git a/images/pattern3.png b/public/images/pattern3.png
similarity index 100%
rename from images/pattern3.png
rename to public/images/pattern3.png
diff --git a/images/pattern4.png b/public/images/pattern4.png
similarity index 100%
rename from images/pattern4.png
rename to public/images/pattern4.png
diff --git a/images/pattern5.png b/public/images/pattern5.png
similarity index 100%
rename from images/pattern5.png
rename to public/images/pattern5.png
diff --git a/images/pattern6.png b/public/images/pattern6.png
similarity index 100%
rename from images/pattern6.png
rename to public/images/pattern6.png
diff --git a/images/preview.png b/public/images/preview.png
similarity index 100%
rename from images/preview.png
rename to public/images/preview.png
diff --git a/images/textures/antique-big.jpg b/public/images/textures/antique-big.jpg
similarity index 100%
rename from images/textures/antique-big.jpg
rename to public/images/textures/antique-big.jpg
diff --git a/images/textures/antique-small.jpg b/public/images/textures/antique-small.jpg
similarity index 100%
rename from images/textures/antique-small.jpg
rename to public/images/textures/antique-small.jpg
diff --git a/images/textures/folded-paper-big.jpg b/public/images/textures/folded-paper-big.jpg
similarity index 100%
rename from images/textures/folded-paper-big.jpg
rename to public/images/textures/folded-paper-big.jpg
diff --git a/images/textures/folded-paper-small.jpg b/public/images/textures/folded-paper-small.jpg
similarity index 100%
rename from images/textures/folded-paper-small.jpg
rename to public/images/textures/folded-paper-small.jpg
diff --git a/images/textures/gray-paper.jpg b/public/images/textures/gray-paper.jpg
similarity index 100%
rename from images/textures/gray-paper.jpg
rename to public/images/textures/gray-paper.jpg
diff --git a/images/textures/iran-small.jpg b/public/images/textures/iran-small.jpg
similarity index 100%
rename from images/textures/iran-small.jpg
rename to public/images/textures/iran-small.jpg
diff --git a/images/textures/marble-big.jpg b/public/images/textures/marble-big.jpg
similarity index 100%
rename from images/textures/marble-big.jpg
rename to public/images/textures/marble-big.jpg
diff --git a/images/textures/marble-blue-big.jpg b/public/images/textures/marble-blue-big.jpg
similarity index 100%
rename from images/textures/marble-blue-big.jpg
rename to public/images/textures/marble-blue-big.jpg
diff --git a/images/textures/marble-blue-small.jpg b/public/images/textures/marble-blue-small.jpg
similarity index 100%
rename from images/textures/marble-blue-small.jpg
rename to public/images/textures/marble-blue-small.jpg
diff --git a/images/textures/marble-small.jpg b/public/images/textures/marble-small.jpg
similarity index 100%
rename from images/textures/marble-small.jpg
rename to public/images/textures/marble-small.jpg
diff --git a/images/textures/mars-big.jpg b/public/images/textures/mars-big.jpg
similarity index 100%
rename from images/textures/mars-big.jpg
rename to public/images/textures/mars-big.jpg
diff --git a/images/textures/mars-small.jpg b/public/images/textures/mars-small.jpg
similarity index 100%
rename from images/textures/mars-small.jpg
rename to public/images/textures/mars-small.jpg
diff --git a/images/textures/mauritania-small.jpg b/public/images/textures/mauritania-small.jpg
similarity index 100%
rename from images/textures/mauritania-small.jpg
rename to public/images/textures/mauritania-small.jpg
diff --git a/images/textures/mercury-big.jpg b/public/images/textures/mercury-big.jpg
similarity index 100%
rename from images/textures/mercury-big.jpg
rename to public/images/textures/mercury-big.jpg
diff --git a/images/textures/mercury-small.jpg b/public/images/textures/mercury-small.jpg
similarity index 100%
rename from images/textures/mercury-small.jpg
rename to public/images/textures/mercury-small.jpg
diff --git a/images/textures/ocean.jpg b/public/images/textures/ocean.jpg
similarity index 100%
rename from images/textures/ocean.jpg
rename to public/images/textures/ocean.jpg
diff --git a/images/textures/pergamena-small.jpg b/public/images/textures/pergamena-small.jpg
similarity index 100%
rename from images/textures/pergamena-small.jpg
rename to public/images/textures/pergamena-small.jpg
diff --git a/images/textures/plaster.jpg b/public/images/textures/plaster.jpg
similarity index 100%
rename from images/textures/plaster.jpg
rename to public/images/textures/plaster.jpg
diff --git a/images/textures/soiled-paper-vertical.png b/public/images/textures/soiled-paper-vertical.png
similarity index 100%
rename from images/textures/soiled-paper-vertical.png
rename to public/images/textures/soiled-paper-vertical.png
diff --git a/images/textures/soiled-paper.jpg b/public/images/textures/soiled-paper.jpg
similarity index 100%
rename from images/textures/soiled-paper.jpg
rename to public/images/textures/soiled-paper.jpg
diff --git a/images/textures/spain-small.jpg b/public/images/textures/spain-small.jpg
similarity index 100%
rename from images/textures/spain-small.jpg
rename to public/images/textures/spain-small.jpg
diff --git a/images/textures/timbercut-big.jpg b/public/images/textures/timbercut-big.jpg
similarity index 100%
rename from images/textures/timbercut-big.jpg
rename to public/images/textures/timbercut-big.jpg
diff --git a/images/textures/timbercut-small.jpg b/public/images/textures/timbercut-small.jpg
similarity index 100%
rename from images/textures/timbercut-small.jpg
rename to public/images/textures/timbercut-small.jpg
diff --git a/index.css b/public/index.css
similarity index 100%
rename from index.css
rename to public/index.css
diff --git a/libs/alea.min.js b/public/libs/alea.min.js
similarity index 100%
rename from libs/alea.min.js
rename to public/libs/alea.min.js
diff --git a/libs/d3.min.js b/public/libs/d3.min.js
similarity index 100%
rename from libs/d3.min.js
rename to public/libs/d3.min.js
diff --git a/libs/delaunator.min.js b/public/libs/delaunator.min.js
similarity index 100%
rename from libs/delaunator.min.js
rename to public/libs/delaunator.min.js
diff --git a/libs/dropbox-sdk.min.js b/public/libs/dropbox-sdk.min.js
similarity index 100%
rename from libs/dropbox-sdk.min.js
rename to public/libs/dropbox-sdk.min.js
diff --git a/libs/flatqueue.js b/public/libs/flatqueue.js
similarity index 100%
rename from libs/flatqueue.js
rename to public/libs/flatqueue.js
diff --git a/libs/indexedDB.js b/public/libs/indexedDB.js
similarity index 100%
rename from libs/indexedDB.js
rename to public/libs/indexedDB.js
diff --git a/libs/jquery-3.1.1.min.js b/public/libs/jquery-3.1.1.min.js
similarity index 100%
rename from libs/jquery-3.1.1.min.js
rename to public/libs/jquery-3.1.1.min.js
diff --git a/libs/jquery-ui.css b/public/libs/jquery-ui.css
similarity index 100%
rename from libs/jquery-ui.css
rename to public/libs/jquery-ui.css
diff --git a/libs/jquery-ui.min.js b/public/libs/jquery-ui.min.js
similarity index 100%
rename from libs/jquery-ui.min.js
rename to public/libs/jquery-ui.min.js
diff --git a/libs/jquery.ui.touch-punch.min.js b/public/libs/jquery.ui.touch-punch.min.js
similarity index 100%
rename from libs/jquery.ui.touch-punch.min.js
rename to public/libs/jquery.ui.touch-punch.min.js
diff --git a/libs/jszip.min.js b/public/libs/jszip.min.js
similarity index 100%
rename from libs/jszip.min.js
rename to public/libs/jszip.min.js
diff --git a/libs/lineclip.min.js b/public/libs/lineclip.min.js
similarity index 100%
rename from libs/lineclip.min.js
rename to public/libs/lineclip.min.js
diff --git a/libs/loopsubdivison.min.js b/public/libs/loopsubdivison.min.js
similarity index 100%
rename from libs/loopsubdivison.min.js
rename to public/libs/loopsubdivison.min.js
diff --git a/libs/mapControls.min.js b/public/libs/mapControls.min.js
similarity index 100%
rename from libs/mapControls.min.js
rename to public/libs/mapControls.min.js
diff --git a/libs/objexporter.min.js b/public/libs/objexporter.min.js
similarity index 100%
rename from libs/objexporter.min.js
rename to public/libs/objexporter.min.js
diff --git a/libs/openwidget.min.js b/public/libs/openwidget.min.js
similarity index 100%
rename from libs/openwidget.min.js
rename to public/libs/openwidget.min.js
diff --git a/libs/orbitControls.min.js b/public/libs/orbitControls.min.js
similarity index 100%
rename from libs/orbitControls.min.js
rename to public/libs/orbitControls.min.js
diff --git a/libs/polylabel.min.js b/public/libs/polylabel.min.js
similarity index 100%
rename from libs/polylabel.min.js
rename to public/libs/polylabel.min.js
diff --git a/libs/rgbquant.min.js b/public/libs/rgbquant.min.js
similarity index 100%
rename from libs/rgbquant.min.js
rename to public/libs/rgbquant.min.js
diff --git a/libs/simplify.js b/public/libs/simplify.js
similarity index 100%
rename from libs/simplify.js
rename to public/libs/simplify.js
diff --git a/libs/three.min.js b/public/libs/three.min.js
similarity index 100%
rename from libs/three.min.js
rename to public/libs/three.min.js
diff --git a/libs/tinymce/icons/default/icons.min.js b/public/libs/tinymce/icons/default/icons.min.js
similarity index 100%
rename from libs/tinymce/icons/default/icons.min.js
rename to public/libs/tinymce/icons/default/icons.min.js
diff --git a/libs/tinymce/langs/README.md b/public/libs/tinymce/langs/README.md
similarity index 100%
rename from libs/tinymce/langs/README.md
rename to public/libs/tinymce/langs/README.md
diff --git a/libs/tinymce/license.md b/public/libs/tinymce/license.md
similarity index 100%
rename from libs/tinymce/license.md
rename to public/libs/tinymce/license.md
diff --git a/libs/tinymce/models/dom/model.min.js b/public/libs/tinymce/models/dom/model.min.js
similarity index 100%
rename from libs/tinymce/models/dom/model.min.js
rename to public/libs/tinymce/models/dom/model.min.js
diff --git a/libs/tinymce/plugins/accordion/plugin.min.js b/public/libs/tinymce/plugins/accordion/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/accordion/plugin.min.js
rename to public/libs/tinymce/plugins/accordion/plugin.min.js
diff --git a/libs/tinymce/plugins/advlist/plugin.min.js b/public/libs/tinymce/plugins/advlist/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/advlist/plugin.min.js
rename to public/libs/tinymce/plugins/advlist/plugin.min.js
diff --git a/libs/tinymce/plugins/anchor/plugin.min.js b/public/libs/tinymce/plugins/anchor/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/anchor/plugin.min.js
rename to public/libs/tinymce/plugins/anchor/plugin.min.js
diff --git a/libs/tinymce/plugins/autolink/plugin.min.js b/public/libs/tinymce/plugins/autolink/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/autolink/plugin.min.js
rename to public/libs/tinymce/plugins/autolink/plugin.min.js
diff --git a/libs/tinymce/plugins/autoresize/plugin.min.js b/public/libs/tinymce/plugins/autoresize/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/autoresize/plugin.min.js
rename to public/libs/tinymce/plugins/autoresize/plugin.min.js
diff --git a/libs/tinymce/plugins/autosave/plugin.min.js b/public/libs/tinymce/plugins/autosave/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/autosave/plugin.min.js
rename to public/libs/tinymce/plugins/autosave/plugin.min.js
diff --git a/libs/tinymce/plugins/charmap/plugin.min.js b/public/libs/tinymce/plugins/charmap/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/charmap/plugin.min.js
rename to public/libs/tinymce/plugins/charmap/plugin.min.js
diff --git a/libs/tinymce/plugins/code/plugin.min.js b/public/libs/tinymce/plugins/code/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/code/plugin.min.js
rename to public/libs/tinymce/plugins/code/plugin.min.js
diff --git a/libs/tinymce/plugins/codesample/plugin.min.js b/public/libs/tinymce/plugins/codesample/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/codesample/plugin.min.js
rename to public/libs/tinymce/plugins/codesample/plugin.min.js
diff --git a/libs/tinymce/plugins/directionality/plugin.min.js b/public/libs/tinymce/plugins/directionality/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/directionality/plugin.min.js
rename to public/libs/tinymce/plugins/directionality/plugin.min.js
diff --git a/libs/tinymce/plugins/emoticons/js/emojiimages.js b/public/libs/tinymce/plugins/emoticons/js/emojiimages.js
similarity index 100%
rename from libs/tinymce/plugins/emoticons/js/emojiimages.js
rename to public/libs/tinymce/plugins/emoticons/js/emojiimages.js
diff --git a/libs/tinymce/plugins/emoticons/js/emojiimages.min.js b/public/libs/tinymce/plugins/emoticons/js/emojiimages.min.js
similarity index 100%
rename from libs/tinymce/plugins/emoticons/js/emojiimages.min.js
rename to public/libs/tinymce/plugins/emoticons/js/emojiimages.min.js
diff --git a/libs/tinymce/plugins/emoticons/js/emojis.js b/public/libs/tinymce/plugins/emoticons/js/emojis.js
similarity index 100%
rename from libs/tinymce/plugins/emoticons/js/emojis.js
rename to public/libs/tinymce/plugins/emoticons/js/emojis.js
diff --git a/libs/tinymce/plugins/emoticons/js/emojis.min.js b/public/libs/tinymce/plugins/emoticons/js/emojis.min.js
similarity index 100%
rename from libs/tinymce/plugins/emoticons/js/emojis.min.js
rename to public/libs/tinymce/plugins/emoticons/js/emojis.min.js
diff --git a/libs/tinymce/plugins/emoticons/plugin.min.js b/public/libs/tinymce/plugins/emoticons/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/emoticons/plugin.min.js
rename to public/libs/tinymce/plugins/emoticons/plugin.min.js
diff --git a/libs/tinymce/plugins/fullscreen/plugin.min.js b/public/libs/tinymce/plugins/fullscreen/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/fullscreen/plugin.min.js
rename to public/libs/tinymce/plugins/fullscreen/plugin.min.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ar.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ar.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/ar.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ar.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/bg_BG.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ca.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ca.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/ca.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ca.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/cs.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/cs.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/cs.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/cs.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/da.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/da.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/da.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/da.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/de.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/de.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/de.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/de.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/el.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/el.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/el.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/el.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/en.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/en.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/en.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/en.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/es.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/es.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/es.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/es.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/eu.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/eu.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/eu.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/eu.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/fa.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/fa.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/fa.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/fa.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/fi.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/fi.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/fi.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/fi.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/fr_FR.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/he_IL.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/hi.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/hi.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/hi.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/hi.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/hr.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/hr.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/hr.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/hr.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/hu_HU.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/id.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/id.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/id.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/id.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/it.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/it.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/it.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/it.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ja.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ja.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/ja.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ja.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/kk.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/kk.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/kk.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/kk.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ko_KR.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ms.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ms.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/ms.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ms.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/nb_NO.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/nl.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/nl.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/nl.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/nl.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/pl.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/pl.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/pl.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/pl.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/pt_BR.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/pt_PT.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ro.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ro.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/ro.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ro.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/ru.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/ru.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/ru.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/ru.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/sk.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/sk.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/sk.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/sk.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/sl_SI.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/sv_SE.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/th_TH.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/tr.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/tr.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/tr.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/tr.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/uk.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/uk.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/uk.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/uk.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/vi.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/vi.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/vi.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/vi.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/zh_CN.js
diff --git a/libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js b/public/libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js
similarity index 100%
rename from libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js
rename to public/libs/tinymce/plugins/help/js/i18n/keynav/zh_TW.js
diff --git a/libs/tinymce/plugins/help/plugin.min.js b/public/libs/tinymce/plugins/help/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/help/plugin.min.js
rename to public/libs/tinymce/plugins/help/plugin.min.js
diff --git a/libs/tinymce/plugins/image/plugin.min.js b/public/libs/tinymce/plugins/image/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/image/plugin.min.js
rename to public/libs/tinymce/plugins/image/plugin.min.js
diff --git a/libs/tinymce/plugins/importcss/plugin.min.js b/public/libs/tinymce/plugins/importcss/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/importcss/plugin.min.js
rename to public/libs/tinymce/plugins/importcss/plugin.min.js
diff --git a/libs/tinymce/plugins/insertdatetime/plugin.min.js b/public/libs/tinymce/plugins/insertdatetime/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/insertdatetime/plugin.min.js
rename to public/libs/tinymce/plugins/insertdatetime/plugin.min.js
diff --git a/libs/tinymce/plugins/link/plugin.min.js b/public/libs/tinymce/plugins/link/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/link/plugin.min.js
rename to public/libs/tinymce/plugins/link/plugin.min.js
diff --git a/libs/tinymce/plugins/lists/plugin.min.js b/public/libs/tinymce/plugins/lists/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/lists/plugin.min.js
rename to public/libs/tinymce/plugins/lists/plugin.min.js
diff --git a/libs/tinymce/plugins/media/plugin.min.js b/public/libs/tinymce/plugins/media/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/media/plugin.min.js
rename to public/libs/tinymce/plugins/media/plugin.min.js
diff --git a/libs/tinymce/plugins/nonbreaking/plugin.min.js b/public/libs/tinymce/plugins/nonbreaking/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/nonbreaking/plugin.min.js
rename to public/libs/tinymce/plugins/nonbreaking/plugin.min.js
diff --git a/libs/tinymce/plugins/pagebreak/plugin.min.js b/public/libs/tinymce/plugins/pagebreak/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/pagebreak/plugin.min.js
rename to public/libs/tinymce/plugins/pagebreak/plugin.min.js
diff --git a/libs/tinymce/plugins/preview/plugin.min.js b/public/libs/tinymce/plugins/preview/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/preview/plugin.min.js
rename to public/libs/tinymce/plugins/preview/plugin.min.js
diff --git a/libs/tinymce/plugins/quickbars/plugin.min.js b/public/libs/tinymce/plugins/quickbars/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/quickbars/plugin.min.js
rename to public/libs/tinymce/plugins/quickbars/plugin.min.js
diff --git a/libs/tinymce/plugins/save/plugin.min.js b/public/libs/tinymce/plugins/save/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/save/plugin.min.js
rename to public/libs/tinymce/plugins/save/plugin.min.js
diff --git a/libs/tinymce/plugins/searchreplace/plugin.min.js b/public/libs/tinymce/plugins/searchreplace/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/searchreplace/plugin.min.js
rename to public/libs/tinymce/plugins/searchreplace/plugin.min.js
diff --git a/libs/tinymce/plugins/table/plugin.min.js b/public/libs/tinymce/plugins/table/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/table/plugin.min.js
rename to public/libs/tinymce/plugins/table/plugin.min.js
diff --git a/libs/tinymce/plugins/visualblocks/plugin.min.js b/public/libs/tinymce/plugins/visualblocks/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/visualblocks/plugin.min.js
rename to public/libs/tinymce/plugins/visualblocks/plugin.min.js
diff --git a/libs/tinymce/plugins/visualchars/plugin.min.js b/public/libs/tinymce/plugins/visualchars/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/visualchars/plugin.min.js
rename to public/libs/tinymce/plugins/visualchars/plugin.min.js
diff --git a/libs/tinymce/plugins/wordcount/plugin.min.js b/public/libs/tinymce/plugins/wordcount/plugin.min.js
similarity index 100%
rename from libs/tinymce/plugins/wordcount/plugin.min.js
rename to public/libs/tinymce/plugins/wordcount/plugin.min.js
diff --git a/libs/tinymce/skins/content/dark/content.js b/public/libs/tinymce/skins/content/dark/content.js
similarity index 100%
rename from libs/tinymce/skins/content/dark/content.js
rename to public/libs/tinymce/skins/content/dark/content.js
diff --git a/libs/tinymce/skins/content/dark/content.min.css b/public/libs/tinymce/skins/content/dark/content.min.css
similarity index 100%
rename from libs/tinymce/skins/content/dark/content.min.css
rename to public/libs/tinymce/skins/content/dark/content.min.css
diff --git a/libs/tinymce/skins/content/default/content.css b/public/libs/tinymce/skins/content/default/content.css
similarity index 100%
rename from libs/tinymce/skins/content/default/content.css
rename to public/libs/tinymce/skins/content/default/content.css
diff --git a/libs/tinymce/skins/content/default/content.js b/public/libs/tinymce/skins/content/default/content.js
similarity index 100%
rename from libs/tinymce/skins/content/default/content.js
rename to public/libs/tinymce/skins/content/default/content.js
diff --git a/libs/tinymce/skins/content/document/content.js b/public/libs/tinymce/skins/content/document/content.js
similarity index 100%
rename from libs/tinymce/skins/content/document/content.js
rename to public/libs/tinymce/skins/content/document/content.js
diff --git a/libs/tinymce/skins/content/document/content.min.css b/public/libs/tinymce/skins/content/document/content.min.css
similarity index 100%
rename from libs/tinymce/skins/content/document/content.min.css
rename to public/libs/tinymce/skins/content/document/content.min.css
diff --git a/libs/tinymce/skins/content/tinymce-5-dark/content.js b/public/libs/tinymce/skins/content/tinymce-5-dark/content.js
similarity index 100%
rename from libs/tinymce/skins/content/tinymce-5-dark/content.js
rename to public/libs/tinymce/skins/content/tinymce-5-dark/content.js
diff --git a/libs/tinymce/skins/content/tinymce-5-dark/content.min.css b/public/libs/tinymce/skins/content/tinymce-5-dark/content.min.css
similarity index 100%
rename from libs/tinymce/skins/content/tinymce-5-dark/content.min.css
rename to public/libs/tinymce/skins/content/tinymce-5-dark/content.min.css
diff --git a/libs/tinymce/skins/content/tinymce-5/content.js b/public/libs/tinymce/skins/content/tinymce-5/content.js
similarity index 100%
rename from libs/tinymce/skins/content/tinymce-5/content.js
rename to public/libs/tinymce/skins/content/tinymce-5/content.js
diff --git a/libs/tinymce/skins/content/tinymce-5/content.min.css b/public/libs/tinymce/skins/content/tinymce-5/content.min.css
similarity index 100%
rename from libs/tinymce/skins/content/tinymce-5/content.min.css
rename to public/libs/tinymce/skins/content/tinymce-5/content.min.css
diff --git a/libs/tinymce/skins/content/writer/content.js b/public/libs/tinymce/skins/content/writer/content.js
similarity index 100%
rename from libs/tinymce/skins/content/writer/content.js
rename to public/libs/tinymce/skins/content/writer/content.js
diff --git a/libs/tinymce/skins/content/writer/content.min.css b/public/libs/tinymce/skins/content/writer/content.min.css
similarity index 100%
rename from libs/tinymce/skins/content/writer/content.min.css
rename to public/libs/tinymce/skins/content/writer/content.min.css
diff --git a/libs/tinymce/skins/ui/oxide-dark/content.inline.js b/public/libs/tinymce/skins/ui/oxide-dark/content.inline.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/content.inline.js
rename to public/libs/tinymce/skins/ui/oxide-dark/content.inline.js
diff --git a/libs/tinymce/skins/ui/oxide-dark/content.inline.min.css b/public/libs/tinymce/skins/ui/oxide-dark/content.inline.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/content.inline.min.css
rename to public/libs/tinymce/skins/ui/oxide-dark/content.inline.min.css
diff --git a/libs/tinymce/skins/ui/oxide-dark/content.js b/public/libs/tinymce/skins/ui/oxide-dark/content.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/content.js
rename to public/libs/tinymce/skins/ui/oxide-dark/content.js
diff --git a/libs/tinymce/skins/ui/oxide-dark/content.min.css b/public/libs/tinymce/skins/ui/oxide-dark/content.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/content.min.css
rename to public/libs/tinymce/skins/ui/oxide-dark/content.min.css
diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.js b/public/libs/tinymce/skins/ui/oxide-dark/skin.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/skin.js
rename to public/libs/tinymce/skins/ui/oxide-dark/skin.js
diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.min.css b/public/libs/tinymce/skins/ui/oxide-dark/skin.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/skin.min.css
rename to public/libs/tinymce/skins/ui/oxide-dark/skin.min.css
diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js b/public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js
rename to public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.js
diff --git a/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css b/public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
rename to public/libs/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
diff --git a/libs/tinymce/skins/ui/oxide/content.css b/public/libs/tinymce/skins/ui/oxide/content.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/content.css
rename to public/libs/tinymce/skins/ui/oxide/content.css
diff --git a/libs/tinymce/skins/ui/oxide/content.inline.css b/public/libs/tinymce/skins/ui/oxide/content.inline.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/content.inline.css
rename to public/libs/tinymce/skins/ui/oxide/content.inline.css
diff --git a/libs/tinymce/skins/ui/oxide/content.inline.js b/public/libs/tinymce/skins/ui/oxide/content.inline.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/content.inline.js
rename to public/libs/tinymce/skins/ui/oxide/content.inline.js
diff --git a/libs/tinymce/skins/ui/oxide/content.js b/public/libs/tinymce/skins/ui/oxide/content.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/content.js
rename to public/libs/tinymce/skins/ui/oxide/content.js
diff --git a/libs/tinymce/skins/ui/oxide/skin.css b/public/libs/tinymce/skins/ui/oxide/skin.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/skin.css
rename to public/libs/tinymce/skins/ui/oxide/skin.css
diff --git a/libs/tinymce/skins/ui/oxide/skin.js b/public/libs/tinymce/skins/ui/oxide/skin.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/skin.js
rename to public/libs/tinymce/skins/ui/oxide/skin.js
diff --git a/libs/tinymce/skins/ui/oxide/skin.shadowdom.css b/public/libs/tinymce/skins/ui/oxide/skin.shadowdom.css
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/skin.shadowdom.css
rename to public/libs/tinymce/skins/ui/oxide/skin.shadowdom.css
diff --git a/libs/tinymce/skins/ui/oxide/skin.shadowdom.js b/public/libs/tinymce/skins/ui/oxide/skin.shadowdom.js
similarity index 100%
rename from libs/tinymce/skins/ui/oxide/skin.shadowdom.js
rename to public/libs/tinymce/skins/ui/oxide/skin.shadowdom.js
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.js
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/content.js
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.js
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/content.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/content.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/content.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/content.min.css
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.js
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.js
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.min.css
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.js
diff --git a/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css b/public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css
diff --git a/libs/tinymce/skins/ui/tinymce-5/content.inline.js b/public/libs/tinymce/skins/ui/tinymce-5/content.inline.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/content.inline.js
rename to public/libs/tinymce/skins/ui/tinymce-5/content.inline.js
diff --git a/libs/tinymce/skins/ui/tinymce-5/content.inline.min.css b/public/libs/tinymce/skins/ui/tinymce-5/content.inline.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/content.inline.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5/content.inline.min.css
diff --git a/libs/tinymce/skins/ui/tinymce-5/content.js b/public/libs/tinymce/skins/ui/tinymce-5/content.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/content.js
rename to public/libs/tinymce/skins/ui/tinymce-5/content.js
diff --git a/libs/tinymce/skins/ui/tinymce-5/content.min.css b/public/libs/tinymce/skins/ui/tinymce-5/content.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/content.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5/content.min.css
diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.js b/public/libs/tinymce/skins/ui/tinymce-5/skin.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/skin.js
rename to public/libs/tinymce/skins/ui/tinymce-5/skin.js
diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.min.css b/public/libs/tinymce/skins/ui/tinymce-5/skin.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/skin.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5/skin.min.css
diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js b/public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js
rename to public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.js
diff --git a/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css b/public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css
similarity index 100%
rename from libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css
rename to public/libs/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css
diff --git a/libs/tinymce/themes/silver/theme.min.js b/public/libs/tinymce/themes/silver/theme.min.js
similarity index 100%
rename from libs/tinymce/themes/silver/theme.min.js
rename to public/libs/tinymce/themes/silver/theme.min.js
diff --git a/libs/tinymce/tinymce.d.ts b/public/libs/tinymce/tinymce.d.ts
similarity index 100%
rename from libs/tinymce/tinymce.d.ts
rename to public/libs/tinymce/tinymce.d.ts
diff --git a/libs/tinymce/tinymce.min.js b/public/libs/tinymce/tinymce.min.js
similarity index 100%
rename from libs/tinymce/tinymce.min.js
rename to public/libs/tinymce/tinymce.min.js
diff --git a/libs/umami.js b/public/libs/umami.js
similarity index 100%
rename from libs/umami.js
rename to public/libs/umami.js
diff --git a/main.js b/public/main.js
similarity index 99%
rename from main.js
rename to public/main.js
index 29760109..6da462d5 100644
--- a/main.js
+++ b/public/main.js
@@ -13,12 +13,6 @@ const ERROR = true;
// detect device
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
-// typed arrays max values
-const INT8_MAX = 127;
-const UINT8_MAX = 255;
-const UINT16_MAX = 65535;
-const UINT32_MAX = 4294967295;
-
if (PRODUCTION && "serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("./sw.js").catch(err => {
@@ -91,7 +85,7 @@ let fogging = viewbox
.attr("id", "fogging")
.style("display", "none");
let ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
-let debug = viewbox.append("g").attr("id", "debug");
+var debug = viewbox.append("g").attr("id", "debug");
lakes.append("g").attr("id", "freshwater");
lakes.append("g").attr("id", "salt");
@@ -140,9 +134,9 @@ legend
.on("click", () => clearLegend());
// main data variables
-let grid = {}; // initial graph based on jittered square grid and data
-let pack = {}; // packed graph and data
-let seed;
+var grid = {}; // initial graph based on jittered square grid and data
+var pack = {}; // packed graph and data
+var seed;
let mapId;
let mapHistory = [];
let elSelected;
@@ -202,8 +196,8 @@ let urbanDensity = +byId("urbanDensityInput").value;
applyStoredOptions();
// voronoi graph extension, cannot be changed after generation
-let graphWidth = +mapWidthInput.value;
-let graphHeight = +mapHeightInput.value;
+var graphWidth = +mapWidthInput.value;
+var graphHeight = +mapHeightInput.value;
// svg canvas resolution, can be changed
let svgWidth = graphWidth;
diff --git a/manifest.webmanifest b/public/manifest.webmanifest
similarity index 100%
rename from manifest.webmanifest
rename to public/manifest.webmanifest
diff --git a/modules/biomes.js b/public/modules/biomes.js
similarity index 100%
rename from modules/biomes.js
rename to public/modules/biomes.js
diff --git a/modules/burgs-generator.js b/public/modules/burgs-generator.js
similarity index 100%
rename from modules/burgs-generator.js
rename to public/modules/burgs-generator.js
diff --git a/modules/coa-generator.js b/public/modules/coa-generator.js
similarity index 100%
rename from modules/coa-generator.js
rename to public/modules/coa-generator.js
diff --git a/modules/coa-renderer.js b/public/modules/coa-renderer.js
similarity index 100%
rename from modules/coa-renderer.js
rename to public/modules/coa-renderer.js
diff --git a/modules/cultures-generator.js b/public/modules/cultures-generator.js
similarity index 100%
rename from modules/cultures-generator.js
rename to public/modules/cultures-generator.js
diff --git a/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js
similarity index 100%
rename from modules/dynamic/auto-update.js
rename to public/modules/dynamic/auto-update.js
diff --git a/modules/dynamic/editors/cultures-editor.js b/public/modules/dynamic/editors/cultures-editor.js
similarity index 100%
rename from modules/dynamic/editors/cultures-editor.js
rename to public/modules/dynamic/editors/cultures-editor.js
diff --git a/modules/dynamic/editors/religions-editor.js b/public/modules/dynamic/editors/religions-editor.js
similarity index 100%
rename from modules/dynamic/editors/religions-editor.js
rename to public/modules/dynamic/editors/religions-editor.js
diff --git a/modules/dynamic/editors/states-editor.js b/public/modules/dynamic/editors/states-editor.js
similarity index 100%
rename from modules/dynamic/editors/states-editor.js
rename to public/modules/dynamic/editors/states-editor.js
diff --git a/modules/dynamic/export-json.js b/public/modules/dynamic/export-json.js
similarity index 100%
rename from modules/dynamic/export-json.js
rename to public/modules/dynamic/export-json.js
diff --git a/modules/dynamic/heightmap-selection.js b/public/modules/dynamic/heightmap-selection.js
similarity index 100%
rename from modules/dynamic/heightmap-selection.js
rename to public/modules/dynamic/heightmap-selection.js
diff --git a/modules/dynamic/hierarchy-tree.js b/public/modules/dynamic/hierarchy-tree.js
similarity index 100%
rename from modules/dynamic/hierarchy-tree.js
rename to public/modules/dynamic/hierarchy-tree.js
diff --git a/modules/dynamic/installation.js b/public/modules/dynamic/installation.js
similarity index 100%
rename from modules/dynamic/installation.js
rename to public/modules/dynamic/installation.js
diff --git a/modules/dynamic/overview/charts-overview.js b/public/modules/dynamic/overview/charts-overview.js
similarity index 100%
rename from modules/dynamic/overview/charts-overview.js
rename to public/modules/dynamic/overview/charts-overview.js
diff --git a/modules/dynamic/supporters.js b/public/modules/dynamic/supporters.js
similarity index 100%
rename from modules/dynamic/supporters.js
rename to public/modules/dynamic/supporters.js
diff --git a/modules/features.js b/public/modules/features.js
similarity index 100%
rename from modules/features.js
rename to public/modules/features.js
diff --git a/modules/fonts.js b/public/modules/fonts.js
similarity index 100%
rename from modules/fonts.js
rename to public/modules/fonts.js
diff --git a/modules/io/cloud.js b/public/modules/io/cloud.js
similarity index 100%
rename from modules/io/cloud.js
rename to public/modules/io/cloud.js
diff --git a/modules/io/export.js b/public/modules/io/export.js
similarity index 100%
rename from modules/io/export.js
rename to public/modules/io/export.js
diff --git a/modules/io/load.js b/public/modules/io/load.js
similarity index 100%
rename from modules/io/load.js
rename to public/modules/io/load.js
diff --git a/modules/io/save.js b/public/modules/io/save.js
similarity index 100%
rename from modules/io/save.js
rename to public/modules/io/save.js
diff --git a/modules/lakes.js b/public/modules/lakes.js
similarity index 100%
rename from modules/lakes.js
rename to public/modules/lakes.js
diff --git a/modules/markers-generator.js b/public/modules/markers-generator.js
similarity index 100%
rename from modules/markers-generator.js
rename to public/modules/markers-generator.js
diff --git a/modules/military-generator.js b/public/modules/military-generator.js
similarity index 99%
rename from modules/military-generator.js
rename to public/modules/military-generator.js
index 5aea87db..34c705d8 100644
--- a/modules/military-generator.js
+++ b/public/modules/military-generator.js
@@ -271,7 +271,7 @@ window.Military = (function () {
}
if (node.t > expected) return;
const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
- const candidates = tree.findAll(node.x, node.y, r);
+ const candidates = findAllInQuadtree(node.x, node.y, r, tree);
for (const c of candidates) {
if (c.t < expected && mergeable(node, c)) {
merge(node, c);
diff --git a/modules/names-generator.js b/public/modules/names-generator.js
similarity index 100%
rename from modules/names-generator.js
rename to public/modules/names-generator.js
diff --git a/modules/ocean-layers.js b/public/modules/ocean-layers.js
similarity index 100%
rename from modules/ocean-layers.js
rename to public/modules/ocean-layers.js
diff --git a/modules/provinces-generator.js b/public/modules/provinces-generator.js
similarity index 100%
rename from modules/provinces-generator.js
rename to public/modules/provinces-generator.js
diff --git a/modules/religions-generator.js b/public/modules/religions-generator.js
similarity index 100%
rename from modules/religions-generator.js
rename to public/modules/religions-generator.js
diff --git a/modules/renderers/draw-borders.js b/public/modules/renderers/draw-borders.js
similarity index 100%
rename from modules/renderers/draw-borders.js
rename to public/modules/renderers/draw-borders.js
diff --git a/modules/renderers/draw-burg-icons.js b/public/modules/renderers/draw-burg-icons.js
similarity index 100%
rename from modules/renderers/draw-burg-icons.js
rename to public/modules/renderers/draw-burg-icons.js
diff --git a/modules/renderers/draw-burg-labels.js b/public/modules/renderers/draw-burg-labels.js
similarity index 100%
rename from modules/renderers/draw-burg-labels.js
rename to public/modules/renderers/draw-burg-labels.js
diff --git a/modules/renderers/draw-emblems.js b/public/modules/renderers/draw-emblems.js
similarity index 100%
rename from modules/renderers/draw-emblems.js
rename to public/modules/renderers/draw-emblems.js
diff --git a/modules/renderers/draw-features.js b/public/modules/renderers/draw-features.js
similarity index 100%
rename from modules/renderers/draw-features.js
rename to public/modules/renderers/draw-features.js
diff --git a/modules/renderers/draw-heightmap.js b/public/modules/renderers/draw-heightmap.js
similarity index 100%
rename from modules/renderers/draw-heightmap.js
rename to public/modules/renderers/draw-heightmap.js
diff --git a/modules/renderers/draw-markers.js b/public/modules/renderers/draw-markers.js
similarity index 100%
rename from modules/renderers/draw-markers.js
rename to public/modules/renderers/draw-markers.js
diff --git a/modules/renderers/draw-military.js b/public/modules/renderers/draw-military.js
similarity index 100%
rename from modules/renderers/draw-military.js
rename to public/modules/renderers/draw-military.js
diff --git a/modules/renderers/draw-relief-icons.js b/public/modules/renderers/draw-relief-icons.js
similarity index 100%
rename from modules/renderers/draw-relief-icons.js
rename to public/modules/renderers/draw-relief-icons.js
diff --git a/modules/renderers/draw-scalebar.js b/public/modules/renderers/draw-scalebar.js
similarity index 100%
rename from modules/renderers/draw-scalebar.js
rename to public/modules/renderers/draw-scalebar.js
diff --git a/modules/renderers/draw-state-labels.js b/public/modules/renderers/draw-state-labels.js
similarity index 100%
rename from modules/renderers/draw-state-labels.js
rename to public/modules/renderers/draw-state-labels.js
diff --git a/modules/renderers/draw-temperature.js b/public/modules/renderers/draw-temperature.js
similarity index 100%
rename from modules/renderers/draw-temperature.js
rename to public/modules/renderers/draw-temperature.js
diff --git a/modules/resample.js b/public/modules/resample.js
similarity index 100%
rename from modules/resample.js
rename to public/modules/resample.js
diff --git a/modules/river-generator.js b/public/modules/river-generator.js
similarity index 100%
rename from modules/river-generator.js
rename to public/modules/river-generator.js
diff --git a/modules/routes-generator.js b/public/modules/routes-generator.js
similarity index 100%
rename from modules/routes-generator.js
rename to public/modules/routes-generator.js
diff --git a/modules/states-generator.js b/public/modules/states-generator.js
similarity index 100%
rename from modules/states-generator.js
rename to public/modules/states-generator.js
diff --git a/modules/submap.js b/public/modules/submap.js
similarity index 100%
rename from modules/submap.js
rename to public/modules/submap.js
diff --git a/modules/ui/3d.js b/public/modules/ui/3d.js
similarity index 100%
rename from modules/ui/3d.js
rename to public/modules/ui/3d.js
diff --git a/modules/ui/ai-generator.js b/public/modules/ui/ai-generator.js
similarity index 100%
rename from modules/ui/ai-generator.js
rename to public/modules/ui/ai-generator.js
diff --git a/modules/ui/battle-screen.js b/public/modules/ui/battle-screen.js
similarity index 100%
rename from modules/ui/battle-screen.js
rename to public/modules/ui/battle-screen.js
diff --git a/modules/ui/biomes-editor.js b/public/modules/ui/biomes-editor.js
similarity index 100%
rename from modules/ui/biomes-editor.js
rename to public/modules/ui/biomes-editor.js
diff --git a/modules/ui/burg-editor.js b/public/modules/ui/burg-editor.js
similarity index 100%
rename from modules/ui/burg-editor.js
rename to public/modules/ui/burg-editor.js
diff --git a/modules/ui/burg-group-editor.js b/public/modules/ui/burg-group-editor.js
similarity index 100%
rename from modules/ui/burg-group-editor.js
rename to public/modules/ui/burg-group-editor.js
diff --git a/modules/ui/burgs-overview.js b/public/modules/ui/burgs-overview.js
similarity index 100%
rename from modules/ui/burgs-overview.js
rename to public/modules/ui/burgs-overview.js
diff --git a/modules/ui/coastline-editor.js b/public/modules/ui/coastline-editor.js
similarity index 100%
rename from modules/ui/coastline-editor.js
rename to public/modules/ui/coastline-editor.js
diff --git a/modules/ui/diplomacy-editor.js b/public/modules/ui/diplomacy-editor.js
similarity index 100%
rename from modules/ui/diplomacy-editor.js
rename to public/modules/ui/diplomacy-editor.js
diff --git a/modules/ui/editors.js b/public/modules/ui/editors.js
similarity index 100%
rename from modules/ui/editors.js
rename to public/modules/ui/editors.js
diff --git a/modules/ui/elevation-profile.js b/public/modules/ui/elevation-profile.js
similarity index 100%
rename from modules/ui/elevation-profile.js
rename to public/modules/ui/elevation-profile.js
diff --git a/modules/ui/emblems-editor.js b/public/modules/ui/emblems-editor.js
similarity index 100%
rename from modules/ui/emblems-editor.js
rename to public/modules/ui/emblems-editor.js
diff --git a/modules/ui/general.js b/public/modules/ui/general.js
similarity index 100%
rename from modules/ui/general.js
rename to public/modules/ui/general.js
diff --git a/modules/ui/heightmap-editor.js b/public/modules/ui/heightmap-editor.js
similarity index 100%
rename from modules/ui/heightmap-editor.js
rename to public/modules/ui/heightmap-editor.js
diff --git a/modules/ui/hotkeys.js b/public/modules/ui/hotkeys.js
similarity index 100%
rename from modules/ui/hotkeys.js
rename to public/modules/ui/hotkeys.js
diff --git a/modules/ui/ice-editor.js b/public/modules/ui/ice-editor.js
similarity index 100%
rename from modules/ui/ice-editor.js
rename to public/modules/ui/ice-editor.js
diff --git a/modules/ui/labels-editor.js b/public/modules/ui/labels-editor.js
similarity index 100%
rename from modules/ui/labels-editor.js
rename to public/modules/ui/labels-editor.js
diff --git a/modules/ui/lakes-editor.js b/public/modules/ui/lakes-editor.js
similarity index 100%
rename from modules/ui/lakes-editor.js
rename to public/modules/ui/lakes-editor.js
diff --git a/modules/ui/layers.js b/public/modules/ui/layers.js
similarity index 100%
rename from modules/ui/layers.js
rename to public/modules/ui/layers.js
diff --git a/modules/ui/markers-editor.js b/public/modules/ui/markers-editor.js
similarity index 100%
rename from modules/ui/markers-editor.js
rename to public/modules/ui/markers-editor.js
diff --git a/modules/ui/markers-overview.js b/public/modules/ui/markers-overview.js
similarity index 100%
rename from modules/ui/markers-overview.js
rename to public/modules/ui/markers-overview.js
diff --git a/modules/ui/measurers.js b/public/modules/ui/measurers.js
similarity index 100%
rename from modules/ui/measurers.js
rename to public/modules/ui/measurers.js
diff --git a/modules/ui/military-overview.js b/public/modules/ui/military-overview.js
similarity index 100%
rename from modules/ui/military-overview.js
rename to public/modules/ui/military-overview.js
diff --git a/modules/ui/namesbase-editor.js b/public/modules/ui/namesbase-editor.js
similarity index 100%
rename from modules/ui/namesbase-editor.js
rename to public/modules/ui/namesbase-editor.js
diff --git a/modules/ui/notes-editor.js b/public/modules/ui/notes-editor.js
similarity index 100%
rename from modules/ui/notes-editor.js
rename to public/modules/ui/notes-editor.js
diff --git a/modules/ui/options.js b/public/modules/ui/options.js
similarity index 100%
rename from modules/ui/options.js
rename to public/modules/ui/options.js
diff --git a/modules/ui/provinces-editor.js b/public/modules/ui/provinces-editor.js
similarity index 100%
rename from modules/ui/provinces-editor.js
rename to public/modules/ui/provinces-editor.js
diff --git a/modules/ui/regiment-editor.js b/public/modules/ui/regiment-editor.js
similarity index 100%
rename from modules/ui/regiment-editor.js
rename to public/modules/ui/regiment-editor.js
diff --git a/modules/ui/regiments-overview.js b/public/modules/ui/regiments-overview.js
similarity index 100%
rename from modules/ui/regiments-overview.js
rename to public/modules/ui/regiments-overview.js
diff --git a/modules/ui/relief-editor.js b/public/modules/ui/relief-editor.js
similarity index 99%
rename from modules/ui/relief-editor.js
rename to public/modules/ui/relief-editor.js
index abb800cd..44a2c727 100644
--- a/modules/ui/relief-editor.js
+++ b/public/modules/ui/relief-editor.js
@@ -201,7 +201,7 @@ function editReliefIcon() {
d3.event.on("drag", function () {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
- tree.findAll(p[0], p[1], r).forEach(f => f[2].remove());
+ findAllInQuadtree(p[0], p[1], r, tree).forEach(f => f[2].remove());
});
}
diff --git a/modules/ui/rivers-creator.js b/public/modules/ui/rivers-creator.js
similarity index 100%
rename from modules/ui/rivers-creator.js
rename to public/modules/ui/rivers-creator.js
diff --git a/modules/ui/rivers-editor.js b/public/modules/ui/rivers-editor.js
similarity index 100%
rename from modules/ui/rivers-editor.js
rename to public/modules/ui/rivers-editor.js
diff --git a/modules/ui/rivers-overview.js b/public/modules/ui/rivers-overview.js
similarity index 100%
rename from modules/ui/rivers-overview.js
rename to public/modules/ui/rivers-overview.js
diff --git a/modules/ui/route-group-editor.js b/public/modules/ui/route-group-editor.js
similarity index 100%
rename from modules/ui/route-group-editor.js
rename to public/modules/ui/route-group-editor.js
diff --git a/modules/ui/routes-creator.js b/public/modules/ui/routes-creator.js
similarity index 100%
rename from modules/ui/routes-creator.js
rename to public/modules/ui/routes-creator.js
diff --git a/modules/ui/routes-editor.js b/public/modules/ui/routes-editor.js
similarity index 100%
rename from modules/ui/routes-editor.js
rename to public/modules/ui/routes-editor.js
diff --git a/modules/ui/routes-overview.js b/public/modules/ui/routes-overview.js
similarity index 100%
rename from modules/ui/routes-overview.js
rename to public/modules/ui/routes-overview.js
diff --git a/modules/ui/style-presets.js b/public/modules/ui/style-presets.js
similarity index 100%
rename from modules/ui/style-presets.js
rename to public/modules/ui/style-presets.js
diff --git a/modules/ui/style.js b/public/modules/ui/style.js
similarity index 100%
rename from modules/ui/style.js
rename to public/modules/ui/style.js
diff --git a/modules/ui/submap-tool.js b/public/modules/ui/submap-tool.js
similarity index 100%
rename from modules/ui/submap-tool.js
rename to public/modules/ui/submap-tool.js
diff --git a/modules/ui/temperature-graph.js b/public/modules/ui/temperature-graph.js
similarity index 100%
rename from modules/ui/temperature-graph.js
rename to public/modules/ui/temperature-graph.js
diff --git a/modules/ui/tools.js b/public/modules/ui/tools.js
similarity index 100%
rename from modules/ui/tools.js
rename to public/modules/ui/tools.js
diff --git a/modules/ui/transform-tool.js b/public/modules/ui/transform-tool.js
similarity index 100%
rename from modules/ui/transform-tool.js
rename to public/modules/ui/transform-tool.js
diff --git a/modules/ui/units-editor.js b/public/modules/ui/units-editor.js
similarity index 100%
rename from modules/ui/units-editor.js
rename to public/modules/ui/units-editor.js
diff --git a/modules/ui/world-configurator.js b/public/modules/ui/world-configurator.js
similarity index 100%
rename from modules/ui/world-configurator.js
rename to public/modules/ui/world-configurator.js
diff --git a/modules/ui/zones-editor.js b/public/modules/ui/zones-editor.js
similarity index 100%
rename from modules/ui/zones-editor.js
rename to public/modules/ui/zones-editor.js
diff --git a/modules/zones-generator.js b/public/modules/zones-generator.js
similarity index 100%
rename from modules/zones-generator.js
rename to public/modules/zones-generator.js
diff --git a/styles/ancient.json b/public/styles/ancient.json
similarity index 100%
rename from styles/ancient.json
rename to public/styles/ancient.json
diff --git a/styles/atlas.json b/public/styles/atlas.json
similarity index 100%
rename from styles/atlas.json
rename to public/styles/atlas.json
diff --git a/styles/clean.json b/public/styles/clean.json
similarity index 100%
rename from styles/clean.json
rename to public/styles/clean.json
diff --git a/styles/cyberpunk.json b/public/styles/cyberpunk.json
similarity index 100%
rename from styles/cyberpunk.json
rename to public/styles/cyberpunk.json
diff --git a/styles/darkSeas.json b/public/styles/darkSeas.json
similarity index 100%
rename from styles/darkSeas.json
rename to public/styles/darkSeas.json
diff --git a/styles/default.json b/public/styles/default.json
similarity index 100%
rename from styles/default.json
rename to public/styles/default.json
diff --git a/styles/gloom.json b/public/styles/gloom.json
similarity index 100%
rename from styles/gloom.json
rename to public/styles/gloom.json
diff --git a/styles/light.json b/public/styles/light.json
similarity index 100%
rename from styles/light.json
rename to public/styles/light.json
diff --git a/styles/monochrome.json b/public/styles/monochrome.json
similarity index 100%
rename from styles/monochrome.json
rename to public/styles/monochrome.json
diff --git a/styles/night.json b/public/styles/night.json
similarity index 100%
rename from styles/night.json
rename to public/styles/night.json
diff --git a/styles/pale.json b/public/styles/pale.json
similarity index 100%
rename from styles/pale.json
rename to public/styles/pale.json
diff --git a/styles/watercolor.json b/public/styles/watercolor.json
similarity index 100%
rename from styles/watercolor.json
rename to public/styles/watercolor.json
diff --git a/sw.js b/public/sw.js
similarity index 100%
rename from sw.js
rename to public/sw.js
diff --git a/versioning.js b/public/versioning.js
similarity index 99%
rename from versioning.js
rename to public/versioning.js
index 1c4d005e..6b1f88fd 100644
--- a/versioning.js
+++ b/public/versioning.js
@@ -13,7 +13,7 @@
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
*/
-const VERSION = "1.109.5";
+const VERSION = "1.110.0";
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
{
diff --git a/run_php_server.bat b/run_php_server.bat
deleted file mode 100644
index e168921d..00000000
--- a/run_php_server.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-start chrome.exe http://localhost:3000/
-@echo off
-php -S localhost:3000
diff --git a/run_python_server.bat b/run_python_server.bat
deleted file mode 100644
index b74d34c1..00000000
--- a/run_python_server.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-start chrome.exe http://localhost:8000/
-@echo off
-python -m http.server 8000
\ No newline at end of file
diff --git a/run_python_server.sh b/run_python_server.sh
deleted file mode 100644
index 7ac82957..00000000
--- a/run_python_server.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env sh
-if command -v python3 >/dev/null 2>&1; then
- PYTHON=python3
-elif command -v python >/dev/null 2>&1; then
- PYTHON=python
-else
- echo "Neither 'python' nor 'python3' was found. Please install Python 3 package." >&2
- exit 1
-fi
-
-chromium http://localhost:8000
-
-$PYTHON -m http.server 8000
diff --git a/index.html b/src/index.html
similarity index 99%
rename from index.html
rename to src/index.html
index b6dfe265..9e892b63 100644
--- a/index.html
+++ b/src/index.html
@@ -8464,54 +8464,39 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/heightmap-generator.ts b/src/modules/heightmap-generator.ts
new file mode 100644
index 00000000..eb48f9f4
--- /dev/null
+++ b/src/modules/heightmap-generator.ts
@@ -0,0 +1,582 @@
+import Alea from "alea";
+import { range as d3Range, leastIndex, mean } from "d3";
+import { createTypedArray, byId, findGridCell, getNumberInRange, lim, minmax, P, rand } from "../utils";
+
+declare global {
+ interface Window {
+ HeightmapGenerator: HeightmapGenerator;
+ }
+ var heightmapTemplates: any;
+ var TIME: boolean;
+ var ERROR: boolean;
+}
+
+type Tool = "Hill" | "Pit" | "Range" | "Trough" | "Strait" | "Mask" | "Invert" | "Add" | "Multiply" | "Smooth";
+
+class HeightmapGenerator {
+ grid: any = null;
+ heights: Uint8Array | null = null;
+ blobPower: number = 0;
+ linePower: number = 0;
+
+ // TODO: remove after migration to TS and use param in constructor
+ get seed() {
+ return (window as any).seed;
+ }
+ get graphWidth() {
+ return (window as any).graphWidth;
+ }
+ get graphHeight() {
+ return (window as any).graphHeight;
+ }
+
+ constructor() {
+
+ }
+
+ private clearData() {
+ this.heights = null;
+ this.grid = null;
+ };
+
+
+ private getBlobPower(cells: number): number {
+ const blobPowerMap: Record = {
+ 1000: 0.93,
+ 2000: 0.95,
+ 5000: 0.97,
+ 10000: 0.98,
+ 20000: 0.99,
+ 30000: 0.991,
+ 40000: 0.993,
+ 50000: 0.994,
+ 60000: 0.995,
+ 70000: 0.9955,
+ 80000: 0.996,
+ 90000: 0.9964,
+ 100000: 0.9973
+ };
+ return blobPowerMap[cells] || 0.98;
+ }
+
+ private getLinePower(cells: number): number {
+ const linePowerMap: Record = {
+ 1000: 0.75,
+ 2000: 0.77,
+ 5000: 0.79,
+ 10000: 0.81,
+ 20000: 0.82,
+ 30000: 0.83,
+ 40000: 0.84,
+ 50000: 0.86,
+ 60000: 0.87,
+ 70000: 0.88,
+ 80000: 0.91,
+ 90000: 0.92,
+ 100000: 0.93
+ };
+
+ return linePowerMap[cells] || 0.81;
+ }
+
+ private getPointInRange(range: string, length: number): number | undefined {
+ if (typeof range !== "string") {
+ window.ERROR && console.error("Range should be a string");
+ return;
+ }
+
+ const min = parseInt(range.split("-")[0]) / 100 || 0;
+ const max = parseInt(range.split("-")[1]) / 100 || min;
+ return rand(min * length, max * length);
+ }
+
+ setGraph(graph: any) {
+ const {cellsDesired, cells, points} = graph;
+ this.heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length}) as Uint8Array;
+ this.blobPower = this.getBlobPower(cellsDesired);
+ this.linePower = this.getLinePower(cellsDesired);
+ this.grid = graph;
+ };
+
+ addHill(count: string, height: string, rangeX: string, rangeY: string): void {
+ const addOneHill = () => {
+ if(!this.heights || !this.grid) return;
+ const change = new Uint8Array(this.heights.length);
+ let limit = 0;
+ let start: number;
+ let h = lim(getNumberInRange(height));
+
+ do {
+ const x = this.getPointInRange(rangeX, this.graphWidth);
+ const y = this.getPointInRange(rangeY, this.graphHeight);
+ if (x === undefined || y === undefined) return;
+ start = findGridCell(x, y, this.grid);
+ limit++;
+ } while (this.heights[start] + h > 90 && limit < 50);
+ change[start] = h;
+ const queue = [start];
+ while (queue.length) {
+ const q = queue.shift() as number;
+
+ for (const c of this.grid.cells.c[q]) {
+ if (change[c]) continue;
+ change[c] = change[q] ** this.blobPower * (Math.random() * 0.2 + 0.9);
+ if (change[c] > 1) queue.push(c);
+ }
+ }
+
+ this.heights = this.heights.map((h, i) => lim(h + change[i]));
+ }
+
+ const desiredHillCount = getNumberInRange(count);
+ for (let i = 0; i < desiredHillCount; i++) {
+ addOneHill();
+ }
+ };
+
+ addPit(count: string, height: string, rangeX: string, rangeY: string): void {
+ const addOnePit = () => {
+ if(!this.heights || !this.grid) return;
+ const used = new Uint8Array(this.heights.length);
+ let limit = 0;
+ let start: number;
+ let h = lim(getNumberInRange(height));
+
+ do {
+ const x = this.getPointInRange(rangeX, this.graphWidth);
+ const y = this.getPointInRange(rangeY, this.graphHeight);
+ if (x === undefined || y === undefined) return;
+ start = findGridCell(x, y, this.grid);
+ limit++;
+ } while (this.heights[start] < 20 && limit < 50);
+
+ const queue = [start];
+ while (queue.length) {
+ const q = queue.shift() as number;
+ h = h ** this.blobPower * (Math.random() * 0.2 + 0.9);
+ if (h < 1) return;
+
+ this.grid.cells.c[q].forEach((c: number) => {
+ if (used[c] || this.heights === null) return;
+ this.heights[c] = lim(this.heights[c] - h * (Math.random() * 0.2 + 0.9));
+ used[c] = 1;
+ queue.push(c);
+ });
+ }
+ }
+
+ const desiredPitCount = getNumberInRange(count);
+ for (let i = 0; i < desiredPitCount; i++) {
+ addOnePit();
+ }
+ };
+
+ addRange(count: string, height: string, rangeX: string, rangeY: string, startCellId?: number, endCellId?: number): void {
+ if(!this.heights || !this.grid) return;
+
+ const addOneRange = () => {
+ if(!this.heights || !this.grid) return;
+
+ // get main ridge
+ const getRange = (cur: number, end: number) => {
+ const range = [cur];
+ const p = this.grid.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ this.grid.cells.c[cur].forEach((e: number) => {
+ if (used[e]) return;
+ let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
+ if (Math.random() > 0.85) diff = diff / 2;
+ if (diff < min) {
+ min = diff;
+ cur = e;
+ }
+ });
+ if (min === Infinity) return range;
+ range.push(cur);
+ used[cur] = 1;
+ }
+
+ return range;
+ }
+
+ const used = new Uint8Array(this.heights.length);
+ let h = lim(getNumberInRange(height));
+
+ if (rangeX && rangeY) {
+ // find start and end points
+ const startX = this.getPointInRange(rangeX, this.graphWidth) as number;
+ const startY = this.getPointInRange(rangeY, this.graphHeight) as number;
+
+ let dist = 0;
+ let limit = 0;
+ let endY;
+ let endX;
+
+ do {
+ endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
+ endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
+ dist = Math.abs(endY - startY) + Math.abs(endX - startX);
+ limit++;
+ } while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 3) && limit < 50);
+
+ startCellId = findGridCell(startX, startY, this.grid);
+ endCellId = findGridCell(endX, endY, this.grid);
+ }
+
+ let range = getRange(startCellId as number, endCellId as number);
+
+
+ // add height to ridge and cells around
+ let queue = range.slice();
+ let i = 0;
+ while (queue.length) {
+ const frontier = queue.slice();
+ (queue = []), i++;
+ frontier.forEach((i: number) => {
+ if(!this.heights) return;
+ this.heights[i] = lim(this.heights[i] + h * (Math.random() * 0.3 + 0.85));
+ });
+ h = h ** this.linePower - 1;
+ if (h < 2) break;
+ frontier.forEach((f: number) => {
+ this.grid.cells.c[f].forEach((i: number) => {
+ if (!used[i]) {
+ queue.push(i);
+ used[i] = 1;
+ }
+ });
+ });
+ }
+
+ // generate prominences
+ range.forEach((cur: number, d: number) => {
+ if (d % 6 !== 0) return;
+ for (const _l of d3Range(i)) {
+ const index = leastIndex(this.grid.cells.c[cur], (a: number, b: number) => this.heights![a] - this.heights![b]);
+ if(index === undefined) continue;
+ const min = this.grid.cells.c[cur][index]; // downhill cell
+ this.heights![min] = (this.heights![cur] * 2 + this.heights![min]) / 3;
+ cur = min;
+ }
+ });
+ }
+
+ const desiredRangeCount = getNumberInRange(count);
+ for (let i = 0; i < desiredRangeCount; i++) {
+ addOneRange();
+ }
+ };
+
+ addTrough(count: string, height: string, rangeX: string, rangeY: string, startCellId?: number, endCellId?: number): void {
+ const addOneTrough = () => {
+ if(!this.heights || !this.grid) return;
+
+ // get main ridge
+ const getRange = (cur: number, end: number) => {
+ const range = [cur];
+ const p = this.grid.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ this.grid.cells.c[cur].forEach((e: number) => {
+ if (used[e]) return;
+ let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
+ if (Math.random() > 0.8) diff = diff / 2;
+ if (diff < min) {
+ min = diff;
+ cur = e;
+ }
+ });
+ if (min === Infinity) return range;
+ range.push(cur);
+ used[cur] = 1;
+ }
+
+ return range;
+ }
+
+ const used = new Uint8Array(this.heights.length);
+ let h = lim(getNumberInRange(height));
+
+ if (rangeX && rangeY) {
+ // find start and end points
+ let limit = 0;
+ let startX: number;
+ let startY: number;
+ let dist = 0;
+ let endX: number;
+ let endY: number;
+ do {
+ startX = this.getPointInRange(rangeX, this.graphWidth) as number;
+ startY = this.getPointInRange(rangeY, this.graphHeight) as number;
+ startCellId = findGridCell(startX, startY, this.grid);
+ limit++;
+ } while (this.heights[startCellId] < 20 && limit < 50);
+
+ limit = 0;
+ do {
+ endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
+ endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
+ dist = Math.abs(endY - startY) + Math.abs(endX - startX);
+ limit++;
+ } while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 2) && limit < 50);
+
+ endCellId = findGridCell(endX, endY, this.grid);
+ }
+
+ let range = getRange(startCellId as number, endCellId as number);
+
+
+ // add height to ridge and cells around
+ let queue = range.slice(),
+ i = 0;
+ while (queue.length) {
+ const frontier = queue.slice();
+ (queue = []), i++;
+ frontier.forEach((i: number) => {
+ this.heights![i] = lim(this.heights![i] - h * (Math.random() * 0.3 + 0.85));
+ });
+ h = h ** this.linePower - 1;
+ if (h < 2) break;
+ frontier.forEach((f: number) => {
+ this.grid.cells.c[f].forEach((i: number) => {
+ if (!used[i]) {
+ queue.push(i);
+ used[i] = 1;
+ }
+ });
+ });
+ }
+
+ // generate prominences
+ range.forEach((cur: number, d: number) => {
+ if (d % 6 !== 0) return;
+ for (const _l of d3Range(i)) {
+ const index = leastIndex(this.grid.cells.c[cur], (a: number, b: number) => this.heights![a] - this.heights![b]);
+ if(index === undefined) continue;
+ const min = this.grid.cells.c[cur][index]; // downhill cell
+ //debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
+ this.heights![min] = (this.heights![cur] * 2 + this.heights![min]) / 3;
+ cur = min;
+ }
+ });
+ }
+
+ const desiredTroughCount = getNumberInRange(count);
+ for(let i = 0; i < desiredTroughCount; i++) {
+ addOneTrough();
+ }
+ };
+
+ addStrait(width: string, direction = "vertical"): void {
+ if(!this.heights || !this.grid) return;
+ const desiredWidth = Math.min(getNumberInRange(width), this.grid.cellsX / 3);
+ if (desiredWidth < 1 && P(desiredWidth)) return;
+ const used = new Uint8Array(this.heights.length);
+ const vert = direction === "vertical";
+ const startX = vert ? Math.floor(Math.random() * this.graphWidth * 0.4 + this.graphWidth * 0.3) : 5;
+ const startY = vert ? 5 : Math.floor(Math.random() * this.graphHeight * 0.4 + this.graphHeight * 0.3);
+ const endX = vert
+ ? Math.floor(this.graphWidth - startX - this.graphWidth * 0.1 + Math.random() * this.graphWidth * 0.2)
+ : this.graphWidth - 5;
+ const endY = vert
+ ? this.graphHeight - 5
+ : Math.floor(this.graphHeight - startY - this.graphHeight * 0.1 + Math.random() * this.graphHeight * 0.2);
+
+ const start = findGridCell(startX, startY, this.grid);
+ const end = findGridCell(endX, endY, this.grid);
+
+ const getRange = (cur: number, end: number) => {
+ const range = [];
+ const p = this.grid.points;
+
+ while (cur !== end) {
+ let min = Infinity;
+ this.grid.cells.c[cur].forEach((e: number) => {
+ let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
+ if (Math.random() > 0.8) diff = diff / 2;
+ if (diff < min) {
+ min = diff;
+ cur = e;
+ }
+ });
+ range.push(cur);
+ }
+
+ return range;
+ }
+ let range = getRange(start, end);
+ const query: number[] = [];
+
+
+ const step = 0.1 / desiredWidth;
+
+ for(let i = 0; i < desiredWidth; i++) {
+ const exp = 0.9 - step * desiredWidth;
+ range.forEach((r: number) => {
+ this.grid.cells.c[r].forEach((e: number) => {
+ if (used[e]) return;
+ used[e] = 1;
+ query.push(e);
+ this.heights![e] **= exp;
+ if (this.heights![e] > 100) this.heights![e] = 5;
+ });
+ });
+ range = query.slice();
+ }
+ };
+
+ modify(range: string, add: number, mult: number, power?: number): void {
+ if(!this.heights) return;
+ const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
+ const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
+ const isLand = min === 20;
+
+ this.heights = this.heights.map(h => {
+ if (h < min || h > max) return h;
+
+ if (add) h = isLand ? Math.max(h + add, 20) : h + add;
+ if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
+ if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
+ return lim(h);
+ });
+ };
+
+ smooth(fr = 2, add = 0): void {
+ if(!this.heights || !this.grid) return;
+ this.heights = this.heights.map((h, i) => {
+ const a = [h];
+ this.grid.cells.c[i].forEach((c: number) => a.push(this.heights![c]));
+ if (fr === 1) return (mean(a) as number) + add;
+ return lim((h * (fr - 1) + (mean(a) as number) + add) / fr);
+ });
+ };
+
+ mask(power = 1): void {
+ if(!this.heights || !this.grid) return;
+ const fr = power ? Math.abs(power) : 1;
+
+ this.heights = this.heights.map((h, i) => {
+ const [x, y] = this.grid.points[i];
+ const nx = (2 * x) / this.graphWidth - 1; // [-1, 1], 0 is center
+ const ny = (2 * y) / this.graphHeight - 1; // [-1, 1], 0 is center
+ let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
+ if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
+ const masked = h * distance;
+ return lim((h * (fr - 1) + masked) / fr);
+ });
+ };
+
+ invert(count: number, axes: string): void {
+ if (!P(count) || !this.heights || !this.grid) return;
+
+ const invertX = axes !== "y";
+ const invertY = axes !== "x";
+ const {cellsX, cellsY} = this.grid;
+
+ const inverted = this.heights.map((_h: number, i: number) => {
+ if(!this.heights) return 0;
+ const x = i % cellsX;
+ const y = Math.floor(i / cellsX);
+
+ const nx = invertX ? cellsX - x - 1 : x;
+ const ny = invertY ? cellsY - y - 1 : y;
+ const invertedI = nx + ny * cellsX;
+ return this.heights[invertedI];
+ });
+
+ this.heights = inverted;
+ };
+
+ addStep(tool: Tool, a2: string, a3: string, a4: string, a5: string): void {
+ if (tool === "Hill") return this.addHill(a2, a3, a4, a5);
+ if (tool === "Pit") return this.addPit(a2, a3, a4, a5);
+ if (tool === "Range") return this.addRange(a2, a3, a4, a5);
+ if (tool === "Trough") return this.addTrough(a2, a3, a4, a5);
+ if (tool === "Strait") return this.addStrait(a2, a3);
+ if (tool === "Mask") return this.mask(+a2);
+ if (tool === "Invert") return this.invert(+a2, a3);
+ if (tool === "Add") return this.modify(a3, +a2, 1);
+ if (tool === "Multiply") return this.modify(a3, 0, +a2);
+ if (tool === "Smooth") return this.smooth(+a2);
+ }
+
+ async generate(graph: any): Promise {
+ TIME && console.time("defineHeightmap");
+ const id = (byId("templateInput")! as HTMLInputElement).value;
+
+ Math.random = Alea(this.seed);
+ const isTemplate = id in heightmapTemplates;
+
+ const heights = isTemplate ? this.fromTemplate(graph, id) : await this.fromPrecreated(graph, id);
+ TIME && console.timeEnd("defineHeightmap");
+
+ this.clearData();
+ return heights as Uint8Array;
+ }
+
+ fromTemplate(graph: any, id: string): Uint8Array | null {
+ const templateString = heightmapTemplates[id]?.template || "";
+ const steps = templateString.split("\n");
+
+ if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
+ this.setGraph(graph);
+
+ for (const step of steps) {
+ const elements = step.trim().split(" ");
+ if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
+ this.addStep(...elements as [Tool, string, string, string, string]);
+ }
+
+ return this.heights;
+ };
+
+ private getHeightsFromImageData(imageData: Uint8ClampedArray): void {
+ if(!this.heights) return;
+ for (let i = 0; i < this.heights.length; i++) {
+ const lightness = imageData[i * 4] / 255;
+ const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
+ this.heights[i] = minmax(Math.floor(powered * 100), 0, 100);
+ }
+ }
+
+ fromPrecreated(graph: any, id: string): Promise {
+ return new Promise(resolve => {
+ // create canvas where 1px corresponds to a cell
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
+ const {cellsX, cellsY} = graph;
+ canvas.width = cellsX;
+ canvas.height = cellsY;
+
+ // load heightmap into image and render to canvas
+ const img = new Image();
+ img.src = `./heightmaps/${id}.png`;
+ img.onload = () => {
+ if(!ctx) {
+ throw new Error("Could not get canvas context");
+ }
+ if(!this.heights) {
+ throw new Error("Heights array is not initialized");
+ }
+ ctx.drawImage(img, 0, 0, cellsX, cellsY);
+ const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
+ this.setGraph(graph);
+ this.getHeightsFromImageData(imageData.data);
+ canvas.remove();
+ img.remove();
+ resolve(this.heights);
+ };
+ });
+ };
+
+ getHeights() {
+ return this.heights;
+ }
+}
+
+window.HeightmapGenerator = new HeightmapGenerator();
\ No newline at end of file
diff --git a/src/modules/index.ts b/src/modules/index.ts
new file mode 100644
index 00000000..fe1135c0
--- /dev/null
+++ b/src/modules/index.ts
@@ -0,0 +1,2 @@
+import "./voronoi";
+import "./heightmap-generator";
\ No newline at end of file
diff --git a/modules/voronoi.js b/src/modules/voronoi.ts
similarity index 60%
rename from modules/voronoi.js
rename to src/modules/voronoi.ts
index 19581c9d..55ac77ab 100644
--- a/modules/voronoi.js
+++ b/src/modules/voronoi.ts
@@ -1,17 +1,27 @@
-class Voronoi {
- /**
- * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
- * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
- * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
- * @param {[number, number][]} points A list of coordinates.
- * @param {number} pointsN The number of points.
- */
- constructor(delaunay, points, pointsN) {
+import Delaunator from "delaunator";
+export type Vertices = { p: Point[], v: number[][], c: number[][] };
+export type Cells = { v: number[][], c: number[][], b: number[], i: Uint32Array } ;
+export type Point = [number, number];
+
+/**
+ * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
+ * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
+ * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
+ * @param {[number, number][]} points A list of coordinates.
+ * @param {number} pointsN The number of points.
+ */
+export class Voronoi {
+ delaunay: Delaunator>
+ points: Point[];
+ pointsN: number;
+ cells: Cells = { v: [], c: [], b: [], i: new Uint32Array() }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell, i = cell indexes;
+ vertices: Vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+
+ constructor(delaunay: Delaunator>, points: Point[], pointsN: number) {
this.delaunay = delaunay;
this.points = points;
this.pointsN = pointsN;
- this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
- this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+ this.vertices
// Half-edges are the indices into the delaunator outputs:
// delaunay.triangles[e] gives the point ID where the half-edge starts
@@ -40,18 +50,18 @@ class Voronoi {
* @param {number} t The index of the triangle
* @returns {[number, number, number]} The IDs of the points comprising the given triangle.
*/
- pointsOfTriangle(t) {
- return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
+ private pointsOfTriangle(triangleIndex: number): [number, number, number] {
+ return this.edgesOfTriangle(triangleIndex).map(edge => this.delaunay.triangles[edge]) as [number, number, number];
}
/**
* Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
- * @param {number} t The index of the triangle
+ * @param {number} triangleIndex The index of the triangle
* @returns {number[]} The indices of the triangles that share half-edges with this triangle.
*/
- trianglesAdjacentToTriangle(t) {
+ private trianglesAdjacentToTriangle(triangleIndex: number): number[] {
let triangles = [];
- for (let edge of this.edgesOfTriangle(t)) {
+ for (let edge of this.edgesOfTriangle(triangleIndex)) {
let opposite = this.delaunay.halfedges[edge];
triangles.push(this.triangleOfEdge(opposite));
}
@@ -61,9 +71,9 @@ class Voronoi {
/**
* Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
* @param {number} start The index of an incoming half-edge that leads to the desired point
- * @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
+ * @returns {[number, number, number]} The indices of all half-edges (incoming or outgoing) that touch the point.
*/
- edgesAroundPoint(start) {
+ private edgesAroundPoint(start: number): [number, number, number] {
const result = [];
let incoming = start;
do {
@@ -71,46 +81,46 @@ class Voronoi {
const outgoing = this.nextHalfedge(incoming);
incoming = this.delaunay.halfedges[outgoing];
} while (incoming !== -1 && incoming !== start && result.length < 20);
- return result;
+ return result as [number, number, number];
}
/**
* Returns the center of the triangle located at the given index.
- * @param {number} t The index of the triangle
- * @returns {[number, number]}
+ * @param {number} triangleIndex The index of the triangle
+ * @returns {[number, number]} The coordinates of the triangle's circumcenter.
*/
- triangleCenter(t) {
- let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
+ private triangleCenter(triangleIndex: number): Point {
+ let vertices = this.pointsOfTriangle(triangleIndex).map(p => this.points[p]);
return this.circumcenter(vertices[0], vertices[1], vertices[2]);
}
/**
- * Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
- * @param {number} t The index of the triangle
+ * Retrieves all of the half-edges for a specific triangle `triangleIndex`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} triangleIndex The index of the triangle
* @returns {[number, number, number]} The edges of the triangle.
*/
- edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
+ private edgesOfTriangle(triangleIndex: number): [number, number, number] { return [3 * triangleIndex, 3 * triangleIndex + 1, 3 * triangleIndex + 2]; }
/**
* Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
* @param {number} e The index of the edge
* @returns {number} The index of the triangle
*/
- triangleOfEdge(e) { return Math.floor(e / 3); }
+ private triangleOfEdge(e: number): number { return Math.floor(e / 3); }
/**
* Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the next half edge
*/
- nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
+ private nextHalfedge(e: number): number { return (e % 3 === 2) ? e - 2 : e + 1; }
/**
* Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the previous half edge
*/
- prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
+ // private prevHalfedge(e: number): number { return (e % 3 === 0) ? e + 2 : e - 1; }
/**
* Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
@@ -119,7 +129,7 @@ class Voronoi {
* @param {[number, number]} c The coordinates of the third point of the triangle
* @return {[number, number]} The coordinates of the circumcenter of the triangle.
*/
- circumcenter(a, b, c) {
+ private circumcenter(a: Point, b: Point, c: Point): Point {
const [ax, ay] = a;
const [bx, by] = b;
const [cx, cy] = c;
@@ -132,4 +142,4 @@ class Voronoi {
Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
];
}
-}
+}
\ No newline at end of file
diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts
new file mode 100644
index 00000000..ad2f9486
--- /dev/null
+++ b/src/utils/arrayUtils.ts
@@ -0,0 +1,107 @@
+/**
+ * Get the last element of an array
+ * @param {Array} array - The array to get the last element from
+ * @returns The last element of the array
+ */
+export const last = (array: T[]): T => {
+ return array[array.length - 1];
+}
+
+/**
+ * Get unique elements from an array
+ * @param {Array} array - The array to get unique elements from
+ * @returns An array with unique elements
+ */
+export const unique = (array: T[]): T[] => {
+ return [...new Set(array)];
+}
+
+/**
+ * Deep copy an object or array
+ * @param {Object|Array} obj - The object or array to deep copy
+ * @returns A deep copy of the object or array
+ */
+export const deepCopy = (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);
+ // don't map keys, probably this is what we would expect
+ const dcMapCore = (m: Map): [any, any][] => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
+
+ const cf: Map any> = new Map any>([
+ [Int8Array, dcTArray],
+ [Uint8Array, dcTArray],
+ [Uint8ClampedArray, dcTArray],
+ [Int16Array, dcTArray],
+ [Uint16Array, dcTArray],
+ [Int32Array, dcTArray],
+ [Uint32Array, dcTArray],
+ [Float32Array, dcTArray],
+ [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]
+ // ... extend here to implement their custom deep copy
+ ]);
+
+ return dcAny(obj);
+}
+
+/**
+ * Get the appropriate typed array constructor based on the maximum value
+ * @param {number} maxValue - The maximum value that will be stored in the array
+ * @returns The typed array constructor
+ */
+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}`
+ );
+
+ 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
+ * @param {Object} options - The options for creating the typed array
+ * @param {number} options.maxValue - The maximum value that will be stored in the array
+ * @param {number} options.length - The length of the typed array to create
+ * @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}): 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
+};
+
+declare global {
+ interface Window {
+ last: typeof last;
+ unique: typeof unique;
+ deepCopy: typeof deepCopy;
+ getTypedArray: typeof getTypedArray;
+ createTypedArray: typeof createTypedArray;
+ INT8_MAX: number;
+ UINT8_MAX: number;
+ UINT16_MAX: number;
+ UINT32_MAX: number;
+ }
+}
diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts
new file mode 100644
index 00000000..047d6eae
--- /dev/null
+++ b/src/utils/colorUtils.ts
@@ -0,0 +1,79 @@
+import { color, interpolate, interpolateRainbow, range, RGBColor, scaleSequential, shuffle } from "d3";
+
+/**
+ * Convert RGB or RGBA color to HEX
+ * @param {string} rgba - The RGB or RGBA color string
+ * @returns {string} - The HEX color string
+ */
+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);
+ 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)
+ : "";
+}
+
+/** Predefined set of 12 distinct colors */
+export const C_12 = [
+ "#dababf",
+ "#fb8072",
+ "#80b1d3",
+ "#fdb462",
+ "#b3de69",
+ "#fccde5",
+ "#c6b9c1",
+ "#bc80bd",
+ "#ccebc5",
+ "#ffed6f",
+ "#8dd3c7",
+ "#eb8de7"
+];
+
+/**
+ * Get an array of distinct colors
+ * @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);
+ const colors = shuffle(
+ 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;
+ return colorFromRainbow.formatHex();
+}
+
+/**
+ * Get a mixed color by blending a given color with a random color
+ * @param {string} color - The base color in HEX format
+ * @param {number} mix - The mix ratio (0 to 1)
+ * @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 => {
+ 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;
+ return mixedColor.brighter(bright).formatHex();
+}
+
+declare global {
+ interface Window {
+ toHEX: typeof toHEX;
+ getColors: typeof getColors;
+ getRandomColor: typeof getRandomColor;
+ getMixedColor: typeof getMixedColor;
+ C_12: typeof C_12;
+ }
+}
diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts
new file mode 100644
index 00000000..d5f2fc9a
--- /dev/null
+++ b/src/utils/commonUtils.ts
@@ -0,0 +1,320 @@
+import { distanceSquared } from "./functionUtils";
+import { rand } from "./probabilityUtils";
+import { rn } from "./numberUtils";
+import { last } from "./arrayUtils";
+
+/**
+ * Clip polygon points to graph boundaries
+ * @param points - Array of points [[x1, y1], [x2, y2], ...]
+ * @param graphWidth - Width of the graph
+ * @param graphHeight - Height of the graph
+ * @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) => {
+ if (points.length < 2) return points;
+ 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
+ * @param points - Array of points defining the polyline
+ * @param point - The point to find the segment for
+ * @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 => {
+ if (points.length === 2) return 1;
+
+ let minSegment = 1;
+ let minDist = Infinity;
+
+ for (let i = 0; i < points.length - 1; i++) {
+ const p1 = points[i];
+ const p2 = points[i + 1];
+
+ const length = Math.sqrt(distanceSquared(p1, p2));
+ const segments = Math.ceil(length / step);
+ const dx = (p2[0] - p1[0]) / segments;
+ const dy = (p2[1] - p1[1]) / segments;
+
+ for (let s = 0; s < segments; s++) {
+ const x = p1[0] + s * dx;
+ const y = p1[1] + s * dy;
+ const dist = distanceSquared(point, [x, y]);
+
+ if (dist >= minDist) continue;
+ minDist = dist;
+ minSegment = i + 1;
+ }
+ }
+
+ return minSegment;
+}
+
+/**
+ * Creates a debounced function that delays invoking func until after ms milliseconds have elapsed
+ * @param func - The function to debounce
+ * @param ms - The number of milliseconds to delay
+ * @returns The debounced function
+ */
+export const debounce = any>(func: T, ms: number) => {
+ let isCooldown = false;
+
+ return function (this: any, ...args: Parameters) {
+ if (isCooldown) return;
+ func.apply(this, args);
+ isCooldown = true;
+ setTimeout(() => (isCooldown = false), ms);
+ };
+}
+
+/**
+ * Creates a throttled function that only invokes func at most once every ms milliseconds
+ * @param func - The function to throttle
+ * @param ms - The number of milliseconds to throttle invocations to
+ * @returns The throttled function
+ */
+export const throttle = any>(func: T, ms: number) => {
+ let isThrottled = false;
+ let savedArgs: any[] | null = null;
+ let savedThis: any = null;
+
+ function wrapper(this: any, ...args: Parameters) {
+ if (isThrottled) {
+ savedArgs = args;
+ savedThis = this;
+ return;
+ }
+
+ func.apply(this, args);
+ isThrottled = true;
+
+ setTimeout(function () {
+ isThrottled = false;
+ if (savedArgs) {
+ wrapper.apply(savedThis, savedArgs as Parameters);
+ savedArgs = savedThis = null;
+ }
+ }, ms);
+ }
+
+ return wrapper;
+}
+
+/**
+ * Parse error to get the readable string in Chrome and Firefox
+ * @param error - The error object to parse
+ * @returns Formatted error string with HTML formatting
+ */
+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 => "" + last(url.split("/")) + "");
+ const errorParsed = errorNoURL.replace(/at /gi, "
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 => {
+ const xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ const reader = new FileReader();
+ reader.onloadend = function () {
+ callback(reader.result);
+ };
+ reader.readAsDataURL(xhr.response);
+ };
+ xhr.open("GET", url);
+ xhr.responseType = "blob";
+ xhr.send();
+}
+
+/**
+ * Open URL in a new tab or window
+ * @param url - The URL to open
+ */
+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");
+}
+
+/**
+ * Wrap URL into html a element
+ * @param URL - The URL for the link
+ * @param description - The link text/description
+ * @returns HTML anchor element as a string
+ */
+export const link = (URL: string, description: string): string => {
+ return `${description}`;
+}
+
+/**
+ * Check if Ctrl key (or Cmd on Mac) was pressed during an event
+ * @param event - The keyboard or mouse event
+ * @returns True if Ctrl/Cmd was pressed
+ */
+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
+ * @param from - Start year (default is 100)
+ * @param to - End year (default is 1000)
+ * @returns Formatted date string
+ */
+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"
+ });
+}
+
+/**
+ * Convert x coordinate to longitude
+ * @param x - The x coordinate
+ * @param mapCoordinates - Map coordinates object with lonW and lonT properties
+ * @param graphWidth - Width of the graph
+ * @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);
+}
+
+/**
+ * Convert y coordinate to latitude
+ * @param y - The y coordinate
+ * @param mapCoordinates - Map coordinates object with latN and latT properties
+ * @param graphHeight - Height of the graph
+ * @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);
+}
+
+/**
+ * Convert x,y coordinates to longitude,latitude
+ * @param x - The x coordinate
+ * @param y - The y coordinate
+ * @param mapCoordinates - Map coordinates object
+ * @param graphWidth - Width of the graph
+ * @param graphHeight - Height of the graph
+ * @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)];
+}
+
+/**
+ * Prompt options interface
+ */
+export interface PromptOptions {
+ default: number | string;
+ step?: number;
+ min?: number;
+ max?: number;
+ required?: boolean;
+}
+
+/**
+ * Initialize custom prompt function (prompt does not work in Electron)
+ * This should be called once when the DOM is ready
+ */
+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};
+
+ (window as any).prompt = function (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");
+
+ const input = prompt.querySelector("#promptInput") as HTMLInputElement;
+ const promptTextElement = prompt.querySelector("#promptText") as HTMLElement;
+
+ if (!input || !promptTextElement) return;
+
+ promptTextElement.innerHTML = promptText;
+
+ const type = typeof options.default === "number" ? "number" : "text";
+ input.type = type;
+
+ if (options.step !== undefined) input.step = options.step.toString();
+ 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.value = options.default.toString();
+ input.style.width = promptText.length > 10 ? "100%" : "auto";
+ prompt.style.display = "block";
+
+ form.addEventListener(
+ "submit",
+ (event: Event) => {
+ event.preventDefault();
+ prompt.style.display = "none";
+ const v = type === "number" ? +input.value : input.value;
+ if (callback) callback(v);
+ },
+ {once: true}
+ );
+ };
+
+ const cancel = prompt.querySelector("#promptCancel");
+ if (cancel) {
+ cancel.addEventListener("click", () => {
+ prompt.style.display = "none";
+ });
+ }
+}
+
+declare global {
+ interface Window {
+ ERROR: boolean;
+ polygonclip: any;
+
+ clipPoly: typeof clipPoly;
+ getSegmentId: typeof getSegmentId;
+ debounce: typeof debounce;
+ throttle: typeof throttle;
+ parseError: typeof parseError;
+ getBase64: typeof getBase64;
+ openURL: typeof openURL;
+ wiki: typeof wiki;
+ link: typeof link;
+ isCtrlClick: typeof isCtrlClick;
+ generateDate: typeof generateDate;
+ getLongitude: typeof getLongitude;
+ getLatitude: typeof getLatitude;
+ getCoordinates: typeof getCoordinates;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/debugUtils.ts b/src/utils/debugUtils.ts
new file mode 100644
index 00000000..6b236ebe
--- /dev/null
+++ b/src/utils/debugUtils.ts
@@ -0,0 +1,114 @@
+import {curveBundle, line, max, min} from "d3";
+import { normalize } from "./numberUtils";
+import { getGridPolygon } from "./graphUtils";
+import { C_12 } from "./colorUtils";
+import { round } from "./stringUtils";
+
+/**
+ * Drawing cell values and polygons for debugging purposes
+ * @param {any[]} data - Array of data values corresponding to each cell
+ * @param {any} packedGraph - The packed graph object containing cell positions
+ */
+export const drawCellsValue = (data: any[], packedGraph: any): void => {
+ window.debug.selectAll("text").remove();
+ window.debug
+ .selectAll("text")
+ .data(data)
+ .enter()
+ .append("text")
+ .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
+ * @param {any} terrs - The SVG group element where the polygons will be drawn
+ */
+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"));
+
+ data = data.map(d => 1 - normalize(d, minimum, maximum));
+ window.debug.selectAll("polygon").remove();
+ window.debug
+ .selectAll("polygon")
+ .data(data)
+ .enter()
+ .append("polygon")
+ .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
+ * @param {any} pack - The packed graph object containing cell positions and routes
+ */
+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 points = packedGraph.cells.p;
+ const links = packedGraph.cells.routes;
+
+ for (const from in links) {
+ for (const to in links[from]) {
+ const [x1, y1] = points[from];
+ const [x3, y3] = points[to];
+ const [x2, y2] = [(x1 + x3) / 2, (y1 + y3) / 2];
+ const routeId = links[from][to];
+
+ routes
+ .append("line")
+ .attr("x1", x1)
+ .attr("y1", y1)
+ .attr("x2", x2)
+ .attr("y2", y2)
+ .attr("data-id", routeId)
+ .attr("stroke", C_12[routeId % 12]);
+ }
+ }
+}
+
+/**
+ * Drawing a point for debugging purposes
+ * @param {[number, number]} point - The [x, y] coordinates of the point to draw
+ * @param {Object} options - Options for drawing the point
+ * @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);
+}
+
+/**
+ * Drawing a path for debugging purposes
+ * @param {[number, number][]} points - Array of [x, y] coordinates representing the path
+ * @param {Object} options - Options for drawing the path
+ * @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 => {
+ const lineGen = line().curve(curveBundle);
+ window.debug
+ .append("path")
+ .attr("d", round(lineGen(points) as string))
+ .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;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/functionUtils.ts b/src/utils/functionUtils.ts
new file mode 100644
index 00000000..5a3d7283
--- /dev/null
+++ b/src/utils/functionUtils.ts
@@ -0,0 +1,64 @@
+/**
+ * Regroup an array of values by multiple keys and reduce each group
+ * @param {Array} values - The array of values to regroup
+ * @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},
+ * {category: 'A', type: 'Y', value: 20},
+ * {category: 'B', type: 'X', value: 30},
+ * {category: 'B', type: 'Y', value: 40},
+ * ];
+ * const result = rollups(
+ * data,
+ * v => v.reduce((sum, d) => sum + d.value, 0),
+ * d => d.category,
+ * d => d.type
+ * );
+ * // result is a Map with structure:
+ * // Map {
+ * // 'A' => Map { 'X' => 10, 'Y' => 20 },
+ * // 'B' => Map { 'X' => 30, 'Y' => 40 }
+ * // }
+ */
+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, 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();
+ const keyof = keys[i++];
+ let index = -1;
+ for (const value of values) {
+ const key = keyof(value, ++index, values);
+ const group = groups.get(key);
+ if (group) group.push(value);
+ else groups.set(key, [value]);
+ }
+ for (const [key, values] of groups) {
+ groups.set(key, regroup(values, i));
+ }
+ return map(groups);
+ })(values, 0);
+}
+
+/**
+ * Calculate squared distance between two points
+ * @param {[number, number]} p1 - First point [x1, y1]
+ * @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]) => {
+ return (x1 - x2) ** 2 + (y1 - y2) ** 2;
+}
+declare global {
+ interface Window {
+ rollups: typeof rollups;
+ dist2: typeof distanceSquared;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts
new file mode 100644
index 00000000..875445fb
--- /dev/null
+++ b/src/utils/graphUtils.ts
@@ -0,0 +1,465 @@
+import Delaunator from "delaunator";
+import Alea from "alea";
+import { color } from "d3";
+import { byId } from "./shorthands";
+import { rn } from "./numberUtils";
+import { createTypedArray } from "./arrayUtils";
+import { Cells, Vertices, Voronoi, Point } from "../modules/voronoi";
+
+/**
+ * Get boundary points on a regular square grid
+ * @param {number} width - The width of the area
+ * @param {number} height - The height of the area
+ * @param {number} spacing - The spacing between points
+ * @returns {Array} - An array of boundary points
+ */
+const getBoundaryPoints = (width: number, height: number, spacing: number): Point[] => {
+ const offset = rn(-1 * spacing);
+ const bSpacing = spacing * 2;
+ const w = width - offset * 2;
+ const h = height - offset * 2;
+ const numberX = Math.ceil(w / bSpacing) - 1;
+ const numberY = Math.ceil(h / bSpacing) - 1;
+ const points: Point[] = [];
+
+ for (let i = 0.5; i < numberX; i++) {
+ let 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);
+ points.push([offset, y], [w + offset, y]);
+ }
+
+ return points;
+}
+
+/**
+ * Get points on a jittered square grid
+ * @param {number} width - The width of the area
+ * @param {number} height - The height of the area
+ * @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 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[] = [];
+ 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);
+ const yj = Math.min(rn(y + jitter(), 2), height);
+ points.push([xj, yj]);
+ }
+ }
+ return points;
+}
+
+/**
+ * Places points on a jittered grid and calculates spacing and cell counts
+ * @param {number} graphWidth - The width of the graph
+ * @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} => {
+ 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
+
+ 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
+ TIME && console.timeEnd("placePoints");
+
+ return {spacing, cellsDesired, boundary, points, cellsX: cellCountX, cellsY: cellCountY};
+}
+
+
+/**
+ * Checks if the grid needs to be regenerated based on desired parameters
+ * @param {Object} grid - The current grid object
+ * @param {number} expectedSeed - The expected seed value
+ * @param {number} graphWidth - The width of the graph
+ * @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) => {
+ 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);
+
+ return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
+}
+
+interface Grid {
+ spacing: number;
+ cellsDesired: number;
+ boundary: Point[];
+ points: Point[];
+ cellsX: number;
+ cellsY: number;
+ seed: string | number;
+ cells: Cells;
+ vertices: Vertices;
+}
+/**
+ * Generates a Voronoi grid based on jittered grid points
+ * @returns {Object} - The generated grid object containing spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, and seed
+ */
+export const generateGrid = (seed: string, graphWidth: number, graphHeight: number): 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};
+}
+
+/**
+ * Calculates the Voronoi diagram from given points and boundary
+ * @param {Array} points - The array of points for Voronoi calculation
+ * @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} => {
+ TIME && console.time("calculateDelaunay");
+ const allPoints = points.concat(boundary);
+ const delaunay = Delaunator.from(allPoints);
+ TIME && console.timeEnd("calculateDelaunay");
+
+ TIME && console.time("calculateVoronoi");
+ const voronoi = new Voronoi(delaunay, allPoints, points.length);
+
+ const cells = voronoi.cells;
+ cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i) as Uint32Array; // array of indexes
+ const vertices = voronoi.vertices;
+ TIME && console.timeEnd("calculateVoronoi");
+
+ return {cells, vertices};
+}
+
+/**
+ * Returns a cell index on a regular square grid based on x and y coordinates
+ * @param {number} x - The x coordinate
+ * @param {number} y - The y coordinate
+ * @param {Object} grid - The grid object containing spacing, cellsX, and cellsY
+ * @returns {number} - The index of the cell in the grid
+ */
+export const findGridCell = (x: number, y: number, grid: any): number => {
+ return (
+ 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
+ * @param {number} radius - The search radius
+ * @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[] => {
+ const c = grid.cells.c;
+ let r = Math.floor(radius / grid.spacing);
+ let found = [findGridCell(x, y, grid)];
+ if (!r || radius === 1) return found;
+ if (r > 0) found = found.concat(c[found[0]]);
+ if (r > 1) {
+ let frontier = c[found[0]];
+ while (r > 1) {
+ let cycle = frontier.slice();
+ frontier = [];
+ cycle.forEach(function (s: number) {
+ c[s].forEach(function (e: number) {
+ if (found.indexOf(e) !== -1) return;
+ found.push(e);
+ frontier.push(e);
+ });
+ });
+ r--;
+ }
+ }
+
+ return found;
+}
+
+/**
+ * Returns the index of the packed cell containing the given x and y coordinates
+ * @param {number} x - The x coordinate
+ * @param {number} y - The y coordinate
+ * @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 => {
+ if (!packedGraph.cells?.q) return;
+ const found = packedGraph.cells.q.find(x, y, radius);
+ return found ? found[2] : undefined;
+}
+
+/**
+ * Returns an array of packed cell indexes within a specified radius from given x and y coordinates
+ * @param {number} x - The x coordinate
+ * @param {number} y - The y coordinate
+ */
+export const findAllCellsInRadius = (x: number, y: number, radius: number, packedGraph: any): number[] => {
+ const found = packedGraph.cells.q.findAll(x, y, radius);
+ return found.map((r: any) => r[2]);
+}
+
+/**
+ * Returns the polygon points for a packed cell given its index
+ * @param {number} i - The index of the packed cell
+ * @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]);
+}
+
+/**
+ * Returns the polygon points for a grid cell given its index
+ * @param {number} i - The index of the grid cell
+ * @returns {Array} - An array of polygon points for the specified grid cell
+ */
+export const getGridPolygon = (i: number, grid: any) => {
+ return grid.cells.v[i].map((v: number) => grid.vertices.p[v]);
+}
+
+/**
+ * mbostock's poissonDiscSampler implementation
+ * Generates points using Poisson-disc sampling within a specified rectangle
+ * @param {number} x0 - The minimum x coordinate of the rectangle
+ * @param {number} y0 - The minimum y coordinate of the rectangle
+ * @param {number} x1 - The maximum x coordinate of the rectangle
+ * @param {number} y1 - The maximum y coordinate of the rectangle
+ * @param {number} r - The minimum distance between points
+ * @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) {
+ if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
+
+ const width = x1 - x0;
+ const height = y1 - y0;
+ const r2 = r * r;
+ const r2_3 = 3 * r2;
+ const cellSize = r * Math.SQRT1_2;
+ const gridWidth = Math.ceil(width / cellSize);
+ const gridHeight = Math.ceil(height / cellSize);
+ const grid = new Array(gridWidth * gridHeight);
+ const queue: [number, number][] = [];
+
+ function far(x: number, y: number) {
+ const i = (x / cellSize) | 0;
+ const j = (y / cellSize) | 0;
+ const i0 = Math.max(i - 2, 0);
+ const j0 = Math.max(j - 2, 0);
+ const i1 = Math.min(i + 3, gridWidth);
+ const j1 = Math.min(j + 3, gridHeight);
+ for (let j = j0; j < j1; ++j) {
+ const o = j * gridWidth;
+ for (let i = i0; i < i1; ++i) {
+ const s = grid[o + i];
+ if (s) {
+ const dx = s[0] - x;
+ const dy = s[1] - y;
+ if (dx * dx + dy * dy < r2) return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ function sample(x: number, y: number) {
+ const point: [number, number] = [x, y];
+ queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = point));
+ return [x + x0, y + y0];
+ }
+
+ yield sample(width / 2, height / 2);
+
+ pick: while (queue.length) {
+ const i = (Math.random() * queue.length) | 0;
+ const parent = queue[i];
+
+ for (let j = 0; j < k; ++j) {
+ const a = 2 * Math.PI * Math.random();
+ const r = Math.sqrt(Math.random() * r2_3 + r2);
+ const x = parent[0] + r * Math.cos(a);
+ const y = parent[1] + r * Math.sin(a);
+ if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
+ yield sample(x, y);
+ continue pick;
+ }
+ }
+
+ const r = queue.pop();
+ if (r !== undefined && i < queue.length) queue[i] = r;
+ }
+}
+
+/**
+ * Checks if a packed cell is land based on its height
+ * @param {number} i - The index of the packed cell
+ * @returns {boolean} - True if the cell is land, false otherwise
+ */
+export const isLand = (i: number, packedGraph: any) => {
+ return packedGraph.cells.h[i] >= 20;
+}
+
+/**
+ * Checks if a packed cell is water based on its height
+ * @param {number} i - The index of the packed cell
+ * @returns {boolean} - True if the cell is water, false otherwise
+ */
+export const isWater = (i: number, packedGraph: any) => {
+ return packedGraph.cells.h[i] < 20;
+}
+
+export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree: any) => {
+ 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.radius = radius * radius;
+ };
+
+ const radiusSearchVisit = (t: any, d2: number) => {
+ t.node.data.scanned = true;
+ if (d2 < t.radius) {
+ do {
+ t.result.push(t.node.data);
+ t.node.data.selected = true;
+ } while ((t.node = t.node.next));
+ }
+ };
+
+ class Quad {
+ node: any;
+ x0: number;
+ y0: number;
+ x1: number;
+ y1: number;
+ constructor(node: any, x0: number, y0: number, x1: number, y1: number) {
+ this.node = node;
+ this.x0 = x0;
+ this.y0 = y0;
+ this.x1 = x1;
+ this.y1 = y1;
+ }
+ }
+
+ 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++;
+
+ // 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
+ )
+ continue;
+
+ // Bisect the current quadrant.
+ if (t.node.length) {
+ t.node.explored = true;
+ var 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)
+ );
+
+ // Visit the closest quadrant first.
+ if ((t.i = (+(y >= ym) << 1) | +(x >= xm))) {
+ 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;
+ }
+ }
+
+ // 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;
+ radiusSearchVisit(t, d2);
+ }
+ }
+ return t.result;
+}
+
+// draw raster heightmap preview (not used in main generation)
+/**
+ * Draws a raster heightmap preview based on given heights and rendering options
+ * @param {Object} options - The options for drawing the heightmap
+ * @param {Array} options.heights - The array of height values
+ * @param {number} options.width - The width of the heightmap
+ * @param {number} options.height - The height of the heightmap
+ * @param {Function} options.scheme - The color scheme function for rendering heights
+ * @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}) => {
+ 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);
+
+ for (let i = 0; i < heights.length; i++) {
+ const colorScheme = scheme(1 - getHeight(heights[i]) / 100);
+ const {r, g, b} = color(colorScheme)!.rgb();
+
+ const n = i * 4;
+ imageData.data[n] = r;
+ imageData.data[n + 1] = g;
+ imageData.data[n + 2] = b;
+ imageData.data[n + 3] = 255;
+ }
+
+ 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;
+ findGridCell: typeof findGridCell;
+ findGridAll: typeof findGridAll;
+ calculateVoronoi: typeof calculateVoronoi;
+ findAll: typeof findAllCellsInRadius;
+ getPackPolygon: typeof getPackPolygon;
+ getGridPolygon: typeof getGridPolygon;
+ poissonDiscSampler: typeof poissonDiscSampler;
+ isLand: typeof isLand;
+ isWater: typeof isWater;
+ findAllInQuadtree: typeof findAllInQuadtree;
+ drawHeights: typeof drawHeights;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 00000000..73581a38
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,236 @@
+import "./polyfills";
+
+import { rn, lim, minmax, normalize, lerp } 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";
+window.vowel = isVowel;
+window.trimVowels = trimVowels;
+window.getAdjective = getAdjective;
+window.nth = nth;
+window.abbreviate = abbreviate;
+window.list = list;
+
+import { last, unique, deepCopy, getTypedArray, createTypedArray, TYPED_ARRAY_MAX_VALUES } from "./arrayUtils";
+window.last = last;
+window.unique = unique;
+window.deepCopy = deepCopy;
+window.getTypedArray = getTypedArray;
+window.createTypedArray = createTypedArray;
+window.INT8_MAX = TYPED_ARRAY_MAX_VALUES.INT8_MAX;
+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";
+window.rand = rand;
+window.P = P;
+window.each = each;
+window.gauss = gauss;
+window.Pint = Pint;
+window.ra = ra;
+window.rw = rw;
+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);
+window.si = si;
+window.getInteger = getIntegerFromSI;
+
+import { toHEX, getColors, getRandomColor, getMixedColor, C_12 } from "./colorUtils";
+window.toHEX = toHEX;
+window.getColors = getColors;
+window.getRandomColor = getRandomColor;
+window.getMixedColor = getMixedColor;
+window.C_12 = C_12;
+
+import { getComposedPath, getNextId } from "./nodeUtils";
+window.getComposedPath = getComposedPath;
+window.getNextId = getNextId;
+
+import { rollups, distanceSquared } from "./functionUtils";
+window.rollups = rollups;
+window.dist2 = distanceSquared;
+
+import { getIsolines, getPolesOfInaccessibility, connectVertices, findPath, 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);
+
+import { round, capitalize, splitInTwo, parseTransform, isValidJSON, safeParseJSON, sanitizeId } from "./stringUtils";
+window.round = round;
+window.capitalize = capitalize;
+window.splitInTwo = splitInTwo;
+window.parseTransform = parseTransform;
+window.sanitizeId = sanitizeId;
+
+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);
+ return this;
+};
+Node.prototype.off = function (name, fn) {
+ this.removeEventListener(name, fn);
+ return this;
+};
+
+declare global {
+
+ interface JSON {
+ isValid: (str: string) => boolean;
+ safeParse: (str: string) => any;
+ }
+
+ interface 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);
+window.calculateVoronoi = calculateVoronoi;
+window.poissonDiscSampler = poissonDiscSampler;
+window.findAllInQuadtree = findAllInQuadtree;
+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, (window as any).graphWidth, (window as any).graphHeight, secure);
+window.getSegmentId = getSegmentId;
+window.debounce = debounce;
+window.throttle = throttle;
+window.parseError = parseError;
+window.getBase64 = getBase64;
+window.openURL = openURL;
+window.wiki = wiki;
+window.link = link;
+window.isCtrlClick = isCtrlClick;
+window.generateDate = generateDate;
+window.getLongitude = (x: number, decimals?: number) => getLongitude(x, (window as any).mapCoordinates, (window as any).graphWidth, decimals);
+window.getLatitude = (y: number, decimals?: number) => getLatitude(y, (window as any).mapCoordinates, (window as any).graphHeight, decimals);
+window.getCoordinates = (x: number, y: number, decimals?: number) => getCoordinates(x, y, (window as any).mapCoordinates, (window as any).graphWidth, (window as any).graphHeight, decimals);
+
+// Initialize prompt when DOM is ready
+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);
+window.drawPoint = drawPoint;
+window.drawPath = drawPath;
+
+
+export {
+ rn,
+ lim,
+ minmax,
+ normalize,
+ lerp,
+ isVowel,
+ trimVowels,
+ getAdjective,
+ nth,
+ abbreviate,
+ list,
+ last,
+ unique,
+ deepCopy,
+ getTypedArray,
+ createTypedArray,
+ TYPED_ARRAY_MAX_VALUES,
+ rand,
+ P,
+ each,
+ gauss,
+ Pint,
+ biased,
+ generateSeed,
+ getNumberInRange,
+ ra,
+ rw,
+ convertTemperature,
+ si,
+ getIntegerFromSI,
+ toHEX,
+ getColors,
+ getRandomColor,
+ getMixedColor,
+ C_12,
+ getComposedPath,
+ getNextId,
+ rollups,
+ distanceSquared,
+ getIsolines,
+ getPolesOfInaccessibility,
+ connectVertices,
+ findPath,
+ getVertexPath,
+ round,
+ capitalize,
+ splitInTwo,
+ parseTransform,
+ isValidJSON,
+ safeParseJSON,
+ sanitizeId,
+ byId,
+ shouldRegenerateGrid,
+ generateGrid,
+ findGridAll,
+ findGridCell,
+ findClosestCell,
+ calculateVoronoi,
+ findAllCellsInRadius,
+ getPackPolygon,
+ getGridPolygon,
+ poissonDiscSampler,
+ isLand,
+ isWater,
+ findAllInQuadtree,
+ drawHeights,
+ clipPoly,
+ getSegmentId,
+ debounce,
+ throttle,
+ parseError,
+ getBase64,
+ openURL,
+ wiki,
+ link,
+ isCtrlClick,
+ generateDate,
+ getLongitude,
+ getLatitude,
+ getCoordinates,
+ initializePrompt,
+ drawCellsValue,
+ drawPolygons,
+ drawRouteConnections,
+ drawPoint,
+ drawPath
+}
\ No newline at end of file
diff --git a/src/utils/languageUtils.ts b/src/utils/languageUtils.ts
new file mode 100644
index 00000000..0fbd20c8
--- /dev/null
+++ b/src/utils/languageUtils.ts
@@ -0,0 +1,217 @@
+import { last } from "./arrayUtils";
+import { P } from "./probabilityUtils";
+
+/**
+ * Check if character is a vowel
+ * @param c - The character to check.
+ * @returns True if the character is a vowel, false otherwise.
+ */
+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.
+ * @param string - The input string.
+ * @param minLength - The minimum length of the string after trimming (default is 3).
+ * @returns The trimmed string.
+ */
+export const trimVowels = (string: string, minLength: number = 3) => {
+ while (string.length > minLength && isVowel(last(Array.from(string)))) {
+ string = string.slice(0, -1);
+ }
+ return string;
+}
+
+
+/**
+ * Get adjective form of a noun based on predefined rules.
+ * @param noun - The noun to be converted to an adjective.
+ * @returns The adjective form of the noun.
+ */
+export const getAdjective = (nounToBeAdjective: string) => {
+ const adjectivizationRules = [
+ {
+ name: "guo",
+ probability: 1,
+ condition: new RegExp(" 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))
+ },
+ {
+ name: "stan",
+ probability: 1,
+ condition: new RegExp("stan$"),
+ action: (noun: string) => (noun.length < 9 ? noun + "i" : trimVowels(noun.slice(0, -4)))
+ },
+ {
+ name: "land",
+ probability: 1,
+ condition: new RegExp("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";
+ }
+ },
+ {
+ name: "que",
+ probability: 1,
+ condition: new RegExp("que$"),
+ action: (noun: string) => noun.replace(/que$/, "can")
+ },
+ {
+ name: "a",
+ probability: 1,
+ condition: new RegExp("a$"),
+ action: (noun: string) => noun + "n"
+ },
+ {
+ name: "o",
+ probability: 1,
+ condition: new RegExp("o$"),
+ action: (noun: string) => noun.replace(/o$/, "an")
+ },
+ {
+ name: "u",
+ probability: 1,
+ condition: new RegExp("u$"),
+ action: (noun: string) => noun + "an"
+ },
+ {
+ name: "i",
+ probability: 1,
+ condition: new RegExp("i$"),
+ action: (noun: string) => noun + "an"
+ },
+ {
+ name: "e",
+ probability: 1,
+ condition: new RegExp("e$"),
+ action: (noun: string) => noun + "an"
+ },
+ {
+ name: "ay",
+ probability: 1,
+ condition: new RegExp("ay$"),
+ action: (noun: string) => noun + "an"
+ },
+ {
+ name: "os",
+ probability: 1,
+ condition: new RegExp("os$"),
+ action: (noun: string) => {
+ const root = trimVowels(noun.slice(0, -2), 0);
+ if (root.length < 4) return noun.slice(0, -1);
+ return root + "ian";
+ }
+ },
+ {
+ name: "es",
+ probability: 1,
+ condition: new RegExp("es$"),
+ action: (noun: string) => {
+ const root = trimVowels(noun.slice(0, -2), 0);
+ if (root.length > 7) return noun.slice(0, -1);
+ return root + "ian";
+ }
+ },
+ {
+ name: "l",
+ probability: 0.8,
+ condition: new RegExp("l$"),
+ action: (noun: string) => noun + "ese"
+ },
+ {
+ name: "n",
+ probability: 0.8,
+ condition: new RegExp("n$"),
+ action: (noun: string) => noun + "ese"
+ },
+ {
+ name: "ad",
+ probability: 0.8,
+ condition: new RegExp("ad$"),
+ action: (noun: string) => noun + "ian"
+ },
+ {
+ name: "an",
+ probability: 0.8,
+ condition: new RegExp("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"
+ },
+ {
+ name: "an",
+ probability: 0.5,
+ condition: new RegExp("^[a-zA-Z]{0,7}$"),
+ action: (noun: string) => trimVowels(noun) + "an"
+ }
+ ];
+ for (const rule of adjectivizationRules) {
+ if (P(rule.probability) && rule.condition.test(nounToBeAdjective)) {
+ return rule.action(nounToBeAdjective);
+ }
+ }
+ 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");
+
+/**
+ * Generate an abbreviation for a given name, avoiding restricted codes.
+ * @param name - The name to be abbreviated.
+ * @param restricted - An array of restricted abbreviations to avoid (default is an empty array).
+ * @returns The generated abbreviation.
+ */
+export const abbreviate = (name: string, restricted: string[] = []) => {
+ const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
+ const words = parsed.split(" ");
+ const letters = words.join("");
+
+ 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.
+ * @param array - The array of strings to be formatted.
+ * @returns The formatted list as a 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"});
+ return conjunction.format(array);
+}
+
+declare global {
+ interface Window {
+ vowel: typeof isVowel;
+ trimVowels: typeof trimVowels;
+ getAdjective: typeof getAdjective;
+ nth: typeof nth;
+ abbreviate: typeof abbreviate;
+ list: typeof list;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/nodeUtils.ts b/src/utils/nodeUtils.ts
new file mode 100644
index 00000000..6213840f
--- /dev/null
+++ b/src/utils/nodeUtils.ts
@@ -0,0 +1,31 @@
+/**
+ * Get the composed path of a node (including shadow DOM and window)
+ * @param {Node | Window} node - The starting node or window
+ * @returns {Array} - The composed path as an array
+ */
+export const getComposedPath = function(node: any): Array {
+ let parent;
+ 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
+ * @param {string} core - The core string for the ID
+ * @param {number} [i=1] - The starting index
+ * @returns {string} - The unique ID
+ */
+export const getNextId = function(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;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/numberUtils.ts b/src/utils/numberUtils.ts
new file mode 100644
index 00000000..a2ab6220
--- /dev/null
+++ b/src/utils/numberUtils.ts
@@ -0,0 +1,62 @@
+/**
+ * Rounds a number to a specified number of decimal places.
+ * @param v - The number to be rounded.
+ * @param d - The number of decimal places to round to (default is 0).
+ * @returns The rounded number.
+ */
+export const rn = (v: number, d: number = 0) => {
+ const m = Math.pow(10, d);
+ return Math.round(v * m) / m;
+}
+
+/**
+ * Clamps a number between a minimum and maximum value.
+ * @param value - The number to be clamped.
+ * @param min - The minimum value.
+ * @param max - The maximum value.
+ * @returns The clamped number.
+ */
+export const minmax = (value: number, min: number, max: number) => {
+ return Math.min(Math.max(value, min), max);
+}
+
+/**
+ * Clamps a number between 0 and 100.
+ * @param v - The number to be clamped.
+ * @returns The clamped 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.
+ * @param val - The number to be normalized.
+ * @param min - The minimum value of the range.
+ * @param max - The maximum value of the range.
+ * @returns The normalized number.
+ */
+export const normalize = (val: number, min: number, max: number) => {
+ return minmax((val - min) / (max - min), 0, 1);
+}
+
+/**
+ * Performs linear interpolation between two values.
+ * @param a - The starting value.
+ * @param b - The ending value.
+ * @param t - The interpolation factor (between 0 and 1).
+ * @returns The interpolated value.
+ */
+export const lerp = (a: number, b: number, t: number) => {
+ return a + (b - a) * t;
+}
+
+declare global {
+ interface Window {
+ rn: typeof rn;
+ minmax: typeof minmax;
+ lim: typeof lim;
+ normalize: typeof normalize;
+ lerp: typeof lerp;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/pathUtils.ts b/src/utils/pathUtils.ts
new file mode 100644
index 00000000..b37f17fb
--- /dev/null
+++ b/src/utils/pathUtils.ts
@@ -0,0 +1,300 @@
+import polylabel from "polylabel";
+import { rn } from "./numberUtils";
+
+/**
+ * Generates SVG path data for filling a shape defined by a chain of vertices.
+ * @param {object} vertices - The vertices object containing positions.
+ * @param {number[]} vertexChain - An array of vertex IDs defining the shape.
+ * @returns {string} SVG path data for the filled shape.
+ */
+const getFillPath = (vertices: any, vertexChain: number[]) => {
+ 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.
+ * @param {object} vertices - The vertices object containing positions.
+ * @param {number[]} vertexChain - An array of vertex IDs defining the border.
+ * @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) => {
+ let discontinued = true;
+ let lastOperation = "";
+ const path = vertexChain.map(vertexId => {
+ if (discontinue(vertexId)) {
+ discontinued = true;
+ return "";
+ }
+
+ const operation = discontinued ? "M" : "L";
+ discontinued = false;
+ 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.
+ * @param {number} exit - The ID of the exit cell.
+ * @param {number} start - The ID of the starting cell.
+ * @param {number[]} from - An array mapping each cell ID to the cell ID it came from.
+ * @returns {number[]} An array of cell IDs representing the path from start to exit.
+ */
+const restorePath = (exit: number, start: number, from: number[]) => {
+ const pathCells = [];
+
+ let current = exit;
+ let prev = exit;
+
+ while (current !== start) {
+ pathCells.push(current);
+ prev = from[current];
+ current = prev;
+ }
+
+ pathCells.push(current);
+
+ return pathCells.reverse();
+}
+
+/**
+ * Returns isolines (borders) for different types of cells in the graph.
+ * @param {object} graph - The graph object containing cells and vertices.
+ * @param {(cellId: number) => any} getType - A function that returns the type of a cell given its ID.
+ * @param {object} [options] - Options to specify which isoline formats to generate.
+ * @param {boolean} [options.polygons=false] - Whether to generate polygons for each type.
+ * @param {boolean} [options.fill=false] - Whether to generate fill paths for each type.
+ * @param {boolean} [options.halo=false] - Whether to generate halo paths for each type.
+ * @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;
+ const isolines: any = {};
+
+ const checkedCells = new Uint8Array(cells.i.length);
+ const addToChecked = (cellId: number) => (checkedCells[cellId] = 1);
+ const isChecked = (cellId: number) => checkedCells[cellId] === 1;
+
+ for (const cellId of cells.i) {
+ if (isChecked(cellId) || !getType(cellId)) continue;
+ addToChecked(cellId);
+
+ const type = getType(cellId);
+ const ofSameType = (cellId: number) => getType(cellId) === type;
+ const ofDifferentType = (cellId: number) => getType(cellId) !== type;
+
+ const onborderCell = cells.c[cellId].find(ofDifferentType);
+ if (onborderCell === undefined) continue;
+
+ // 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;
+
+ 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});
+ if (vertexChain.length < 3) continue;
+
+ addIsolineTo(type, vertices, vertexChain, isolines, options);
+ }
+
+ return isolines;
+
+ 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]));
+ }
+
+ if (options.fill) {
+ if (!isolines[type].fill) isolines[type].fill = "";
+ isolines[type].fill += getFillPath(vertices, vertexChain);
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+ }
+}
+
+
+/**
+ * Generates SVG path data for the border of a shape defined by a chain of vertices.
+ * @param {number[]} cellsArray - An array of cell IDs defining the shape.
+ * @param {object} packedGraph - The packed graph object containing cells and vertices.
+ * @returns {string} SVG path data for the border of the shape.
+ */
+export const getVertexPath = (cellsArray: number[], packedGraph: any = {}) => {
+ const {cells, vertices} = packedGraph;
+
+ 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 isChecked = (cellId: number) => checkedCells[cellId] === 1;
+ let path = "";
+
+ for (const cellId of cellsArray) {
+ if (isChecked(cellId)) continue;
+
+ const onborderCell = cells.c[cellId].find(ofDifferentType);
+ if (onborderCell === undefined) continue;
+
+ const feature = packedGraph.features[cells.f[onborderCell]];
+ if (feature.type === "lake" && feature.shoreline) {
+ 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 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.
+ * @param {object} graph - The graph object containing cells and vertices.
+ * @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});
+
+ const poles = Object.entries(isolines).map(([id, isoline]) => {
+ 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.
+ * @param {object} options - Options for connecting vertices.
+ * @param {object} options.vertices - The vertices object containing connections.
+ * @param {number} options.startingVertex - The ID of the starting vertex.
+ * @param {(cellId: number) => boolean} options.ofSameType - A function that checks if a cell is of the same type.
+ * @param {(cellId: number) => void} [options.addToChecked] - A function to mark cells as checked.
+ * @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}) => {
+ const MAX_ITERATIONS = vertices.c.length;
+ const chain = []; // vertices chain to form a path
+
+ let next = startingVertex;
+ for (let i = 0; i === 0 || next !== startingVertex; i++) {
+ const previous = chain.at(-1);
+ const current = next;
+ chain.push(current);
+
+ const neibCells = vertices.c[current];
+ if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
+
+ const [c1, c2, c3] = neibCells.map(ofSameType);
+ const [v1, v2, v3] = vertices.v[current];
+
+ if (v1 !== previous && c1 !== c2) next = v1;
+ else if (v2 !== previous && c2 !== c3) next = v2;
+ else if (v3 !== previous && c1 !== c3) next = v3;
+
+ if (next >= vertices.c.length) {
+ 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");
+ break;
+ }
+
+ if (i === 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.
+ * @param {number} start - The ID of the starting cell.
+ * @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
+ * @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections.
+ * @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 => {
+ if (isExit(start)) return null;
+
+ const from = [];
+ const cost = [];
+ const queue = new window.FlatQueue();
+ queue.push(start, 0);
+
+ while (queue.length) {
+ const currentCost = queue.peekValue();
+ const current = queue.pop();
+
+ for (const next of packedGraph.cells.c[current]) {
+ if (isExit(next)) {
+ from[next] = current;
+ return restorePath(next, start, from);
+ }
+
+ const nextCost = getCost(current, next);
+ if (nextCost === Infinity) continue; // impassable cell
+ const totalCost = currentCost + nextCost;
+
+ if (totalCost >= cost[next]) continue; // has cheaper path
+ from[next] = current;
+ cost[next] = totalCost;
+ queue.push(next, totalCost);
+ }
+ }
+
+ return null;
+}
+
+declare global {
+ interface Window {
+ ERROR: boolean;
+ FlatQueue: any;
+
+ getIsolines: typeof getIsolines;
+ getPolesOfInaccessibility: typeof getPolesOfInaccessibility;
+ connectVertices: typeof connectVertices;
+ findPath: typeof findPath;
+ getVertexPath: typeof getVertexPath;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/polyfills.ts b/src/utils/polyfills.ts
new file mode 100644
index 00000000..18f5f1bd
--- /dev/null
+++ b/src/utils/polyfills.ts
@@ -0,0 +1,54 @@
+// 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);
+ return this.replace(new RegExp(str, "g"), newStr as any);
+ };
+}
+
+// flat
+if (Array.prototype.flat === undefined) {
+ Array.prototype.flat = function (this: T[], depth?: number): any[] {
+ return (this as Array).reduce((acc: any[], val: unknown) => (Array.isArray(val) ? acc.concat((val as any).flat(depth)) : acc.concat(val)), []);
+ };
+}
+
+// at
+if (Array.prototype.at === undefined) {
+ Array.prototype.at = function (this: T[], index: number): T | undefined {
+ if (index < 0) index += this.length;
+ if (index < 0 || index >= this.length) return undefined;
+ return this[index];
+ };
+}
+
+// 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* (this: ReadableStream): AsyncGenerator {
+ const reader = this.getReader();
+ try {
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) return;
+ yield value;
+ }
+ } finally {
+ reader.releaseLock();
+ }
+ };
+}
+
+declare global {
+ interface String {
+ replaceAll(searchValue: string | RegExp, replaceValue: string | ((substring: string, ...args: any[]) => string)): string;
+ }
+
+ interface Array {
+ flat(depth?: number): T[];
+ at(index: number): T | undefined;
+ }
+
+ interface ReadableStream {
+ [Symbol.asyncIterator](): AsyncIterableIterator;
+ }
+}
diff --git a/src/utils/probabilityUtils.ts b/src/utils/probabilityUtils.ts
new file mode 100644
index 00000000..4f6fd66a
--- /dev/null
+++ b/src/utils/probabilityUtils.ts
@@ -0,0 +1,147 @@
+import { minmax, rn } from "./numberUtils";
+import { randomNormal } from "d3";
+
+/**
+ * Creates a random number between min and max (inclusive).
+ * @param {number} min - minimum value
+ * @param {number} max - maximum value
+ * @return {number} random integer between min and max
+ */
+export const rand = (min: number, max?: number): number => {
+ if (min === undefined && max === undefined) return Math.random();
+ if (max === undefined) {
+ max = min;
+ min = 0;
+ }
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+/**
+ * Returns a boolean based on the given probability.
+ * @param {number} probability - probability between 0 and 1
+ * @return {boolean} true with the given probability
+ */
+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.
+ * @param {number} n - the interval
+ * @return {function} function that takes the current index and returns true every n times
+ */
+export const each = (n: number) => {
+ return (i: number) => i % n === 0;
+}
+
+/**
+ * Random Gaussian number generator
+ * @param {number} expected - expected value
+ * @param {number} deviation - standard deviation
+ * @param {number} min - minimum value
+ * @param {number} max - maximum value
+ * @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) => {
+ return rn(minmax(randomNormal(expected, deviation)(), min, max), round);
+}
+
+/**
+ * Returns the integer part of a float plus one with the probability of the decimal part.
+ * @param {number} float - the float number
+ * @return {number} the resulting integer
+ */
+export const Pint = (float: number): number => {
+ return ~~float + +P(float % 1);
+}
+
+/**
+ * Returns a random element from an array.
+ * @param {Array} array - the array to pick from
+ * @return {any} a random element from the array
+ */
+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 => {
+ const array = [];
+ for (const key in object) {
+ for (let i = 0; i < object[key]; i++) {
+ array.push(key);
+ }
+ }
+ 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).
+ * @param {number} min - minimum value
+ * @param {number} max - maximum value
+ * @param {number} ex - exponent for bias
+ * @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));
+}
+
+const ERROR = false;
+/**
+ * Get number from string in format "1-3" or "2" or "0.5"
+ * @param {string} r - range string
+ * @return {number} parsed number
+ */
+export const getNumberInRange = (r: string): number => {
+ if (typeof r !== "string") {
+ ERROR && console.error("Range value should be a string", r);
+ return 0;
+ }
+ if (!isNaN(+r)) return ~~r + +P(+r - ~~r);
+ const sign = r[0] === "-" ? -1 : 1;
+ if (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) {
+ 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 {
+ rand: typeof rand;
+ P: typeof P;
+ each: typeof each;
+ gauss: typeof gauss;
+ Pint: typeof Pint;
+ ra: typeof ra;
+ rw: typeof rw;
+ biased: typeof biased;
+ getNumberInRange: typeof getNumberInRange;
+ generateSeed: typeof generateSeed;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/shorthands.ts b/src/utils/shorthands.ts
new file mode 100644
index 00000000..79d0866b
--- /dev/null
+++ b/src/utils/shorthands.ts
@@ -0,0 +1,7 @@
+export const byId = document.getElementById.bind(document);
+
+declare global {
+ interface Window {
+ byId: typeof byId;
+ }
+}
diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts
new file mode 100644
index 00000000..02c4d42a
--- /dev/null
+++ b/src/utils/stringUtils.ts
@@ -0,0 +1,125 @@
+import { rn } from "./numberUtils";
+
+/**
+ * Round all numbers in a string to d decimal places
+ * @param {string} inputString - The input string
+ * @param {number} decimals - Number of decimal places (default is 1)
+ * @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 rn(parseFloat(n), decimals).toString();
+ });
+}
+
+/**
+ * Capitalize the first letter of a string
+ * @param {string} inputString - The input string
+ * @returns {string} - The capitalized string
+ */
+export const capitalize = (inputString: string) => {
+ return inputString.charAt(0).toUpperCase() + inputString.slice(1);
+}
+
+/**
+ * Split a string into two parts, trying to balance their lengths
+ * @param {string} inputString - The input string
+ * @returns {[string, string]} - An array with two parts of the string
+ */
+export const splitInTwo = (inputString: string): string[] => {
+ const half = inputString.length / 2;
+ const ar = inputString.split(" ");
+ if (ar.length < 2) return ar; // only one word
+ let first = "",
+ last = "",
+ middle = "",
+ rest = "";
+
+ ar.forEach((w, d) => {
+ if (d + 1 !== ar.length) w += " ";
+ rest += w;
+ if (!first || rest.length < half) first += w;
+ else if (!middle) middle = w;
+ else last += w;
+ });
+
+ 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]
+ */
+export const parseTransform = (string: string) => {
+ if (!string) return [0, 0, 0, 0, 0, 1];
+
+ const a = string
+ .replace(/[a-z()]/g, "")
+ .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
+ * @param {string} str - The string to check
+ * @returns {boolean} - True if the string is valid JSON, false otherwise
+ */
+export const isValidJSON = (str: string): boolean => {
+ try {
+ JSON.parse(str);
+ return true;
+ } catch (e) {
+ return false;
+ }
+};
+
+/**
+ * Safely parse a JSON string
+ * @param {string} str - The JSON string to parse
+ * @returns {any|null} - The parsed object, or null if parsing failed
+ */
+export const safeParseJSON = (str: string) => {
+ try {
+ return JSON.parse(str);
+ } catch (e) {
+ return null;
+ }
+};
+
+/**
+ * Sanitize a string to be used as an ID
+ * @param {string} string - The input string
+ * @returns {string} - The sanitized ID string
+ */
+export const sanitizeId = (inputString: string) => {
+ if (!inputString) throw new Error("No string provided");
+
+ let sanitized = inputString
+ .toLowerCase()
+ .trim()
+ .replace(/[^a-z0-9-_]/g, "") // no invalid characters
+ .replace(/\s+/g, "-"); // replace spaces with hyphens
+
+ // remove leading numbers
+ if (sanitized.match(/^\d/)) sanitized = "_" + sanitized;
+
+ return sanitized;
+}
+
+declare global {
+ interface Window {
+ round: typeof round;
+ capitalize: typeof capitalize;
+ splitInTwo: typeof splitInTwo;
+ parseTransform: typeof parseTransform;
+ sanitizeId: typeof sanitizeId;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/unitUtils.ts b/src/utils/unitUtils.ts
new file mode 100644
index 00000000..072c0b38
--- /dev/null
+++ b/src/utils/unitUtils.ts
@@ -0,0 +1,57 @@
+import { rn } from "./numberUtils";
+
+type TemperatureScale = "°C" | "°F" | "K" | "°R" | "°De" | "°N" | "°Ré" | "°Rø";
+/**
+ * Convert temperature from Celsius to other scales
+ * @param {number} temperatureInCelsius - Temperature in Celsius
+ * @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ø"
+ };
+ return temperatureConversionMap[targetScale](temperatureInCelsius);
+}
+
+/**
+ * Convert number to short string with SI postfix
+ * @param {number} n - The number to convert
+ * @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";
+ return rn(n).toString();
+}
+
+/**
+ * Convert string with SI postfix to integer
+ * @param {string} value - The string to convert
+ * @returns {number} - The converted integer
+ */
+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);
+}
+
+declare global {
+ interface Window {
+ convertTemperature: typeof convertTemperature;
+ si: typeof si;
+ getInteger: typeof getIntegerFromSI;
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..8b583a9d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "target": "esnext",
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+ "isolatedModules": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
\ No newline at end of file
diff --git a/utils/arrayUtils.js b/utils/arrayUtils.js
deleted file mode 100644
index d872d086..00000000
--- a/utils/arrayUtils.js
+++ /dev/null
@@ -1,60 +0,0 @@
-"use strict";
-
-function last(array) {
- return array[array.length - 1];
-}
-
-function unique(array) {
- return [...new Set(array)];
-}
-
-// deep copy for Arrays (and other objects)
-function deepCopy(obj) {
- const id = x => x;
- const dcTArray = a => a.map(id);
- const dcObject = x => Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
- const dcAny = x => (x instanceof Object ? (cf.get(x.constructor) || id)(x) : x);
- // don't map keys, probably this is what we would expect
- const dcMapCore = m => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
-
- const cf = new Map([
- [Int8Array, dcTArray],
- [Uint8Array, dcTArray],
- [Uint8ClampedArray, dcTArray],
- [Int16Array, dcTArray],
- [Uint16Array, dcTArray],
- [Int32Array, dcTArray],
- [Uint32Array, dcTArray],
- [Float32Array, dcTArray],
- [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]
- // ... extend here to implement their custom deep copy
- ]);
-
- return dcAny(obj);
-}
-
-function getTypedArray(maxValue) {
- console.assert(
- Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= UINT32_MAX,
- `Array maxValue must be an integer between 0 and ${UINT32_MAX}, got ${maxValue}`
- );
-
- if (maxValue <= UINT8_MAX) return Uint8Array;
- if (maxValue <= UINT16_MAX) return Uint16Array;
- if (maxValue <= UINT32_MAX) return Uint32Array;
- return Uint32Array;
-}
-
-function createTypedArray({maxValue, length, from}) {
- const typedArray = getTypedArray(maxValue);
- if (!from) return new typedArray(length);
- return typedArray.from(from);
-}
diff --git a/utils/colorUtils.js b/utils/colorUtils.js
deleted file mode 100644
index b96cd79c..00000000
--- a/utils/colorUtils.js
+++ /dev/null
@@ -1,49 +0,0 @@
-"use strict";
-// FMG utils related to colors
-
-// convert RGB color string to HEX without #
-function toHEX(rgb) {
- if (rgb.charAt(0) === "#") return rgb;
-
- rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
- return rgb && rgb.length === 4
- ? "#" +
- ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) +
- ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) +
- ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2)
- : "";
-}
-
-const C_12 = [
- "#dababf",
- "#fb8072",
- "#80b1d3",
- "#fdb462",
- "#b3de69",
- "#fccde5",
- "#c6b9c1",
- "#bc80bd",
- "#ccebc5",
- "#ffed6f",
- "#8dd3c7",
- "#eb8de7"
-];
-const scaleRainbow = d3.scaleSequential(d3.interpolateRainbow);
-
-// return array of standard shuffled colors
-function getColors(number) {
- const colors = d3.shuffle(
- d3.range(number).map(i => (i < 12 ? C_12[i] : d3.color(scaleRainbow((i - 12) / (number - 12))).hex()))
- );
- return colors;
-}
-
-function getRandomColor() {
- return d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
-}
-
-// mix a color with a random color
-function getMixedColor(color, mix = 0.2, bright = 0.3) {
- const c = color && color[0] === "#" ? color : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
- return d3.color(d3.interpolate(c, getRandomColor())(mix)).brighter(bright).hex();
-}
diff --git a/utils/commonUtils.js b/utils/commonUtils.js
deleted file mode 100644
index 58f3f0be..00000000
--- a/utils/commonUtils.js
+++ /dev/null
@@ -1,191 +0,0 @@
-"use strict";
-// FMG helper functions
-
-// clip polygon by graph bbox
-function clipPoly(points, secure = 0) {
- if (points.length < 2) return points;
- if (points.some(point => point === undefined)) {
- ERROR && console.error("Undefined point in clipPoly", points);
- return points;
- }
-
- return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
-}
-
-// get segment of any point on polyline
-function getSegmentId(points, point, step = 10) {
- if (points.length === 2) return 1;
-
- let minSegment = 1;
- let minDist = Infinity;
-
- for (let i = 0; i < points.length - 1; i++) {
- const p1 = points[i];
- const p2 = points[i + 1];
-
- const length = Math.sqrt(dist2(p1, p2));
- const segments = Math.ceil(length / step);
- const dx = (p2[0] - p1[0]) / segments;
- const dy = (p2[1] - p1[1]) / segments;
-
- for (let s = 0; s < segments; s++) {
- const x = p1[0] + s * dx;
- const y = p1[1] + s * dy;
- const dist = dist2(point, [x, y]);
-
- if (dist >= minDist) continue;
- minDist = dist;
- minSegment = i + 1;
- }
- }
-
- return minSegment;
-}
-
-function debounce(func, ms) {
- let isCooldown = false;
-
- return function () {
- if (isCooldown) return;
- func.apply(this, arguments);
- isCooldown = true;
- setTimeout(() => (isCooldown = false), ms);
- };
-}
-
-function throttle(func, ms) {
- let isThrottled = false;
- let savedArgs;
- let savedThis;
-
- function wrapper() {
- if (isThrottled) {
- savedArgs = arguments;
- savedThis = this;
- return;
- }
-
- func.apply(this, arguments);
- isThrottled = true;
-
- setTimeout(function () {
- isThrottled = false;
- if (savedArgs) {
- wrapper.apply(savedThis, savedArgs);
- savedArgs = savedThis = null;
- }
- }, ms);
- }
-
- return wrapper;
-}
-
-// parse error to get the readable string in Chrome and Firefox
-function parseError(error) {
- 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 => "" + last(url.split("/")) + "");
- const errorParsed = errorNoURL.replace(/at /gi, "
at ");
- return errorParsed;
-}
-
-function getBase64(url, callback) {
- const xhr = new XMLHttpRequest();
- xhr.onload = function () {
- const reader = new FileReader();
- reader.onloadend = function () {
- callback(reader.result);
- };
- reader.readAsDataURL(xhr.response);
- };
- xhr.open("GET", url);
- xhr.responseType = "blob";
- xhr.send();
-}
-
-// open URL in a new tab or window
-function openURL(url) {
- window.open(url, "_blank");
-}
-
-// open project wiki-page
-function wiki(page) {
- window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
-}
-
-// wrap URL into html a element
-function link(URL, description) {
- return `${description}`;
-}
-
-function isCtrlClick(event) {
- // meta key is cmd key on MacOs
- return event.ctrlKey || event.metaKey;
-}
-
-function generateDate(from = 100, to = 1000) {
- return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {
- year: "numeric",
- month: "long",
- day: "numeric"
- });
-}
-
-function getLongitude(x, decimals = 2) {
- return rn(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, decimals);
-}
-
-function getLatitude(y, decimals = 2) {
- return rn(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, decimals);
-}
-
-function getCoordinates(x, y, decimals = 2) {
- return [getLongitude(x, decimals), getLatitude(y, decimals)];
-}
-
-// prompt replacer (prompt does not work in Electron)
-void (function () {
- const prompt = document.getElementById("prompt");
- const form = prompt.querySelector("#promptForm");
-
- const defaultText = "Please provide an input";
- const defaultOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
-
- window.prompt = function (promptText = defaultText, options = defaultOptions, callback) {
- if (options.default === undefined)
- return ERROR && console.error("Prompt: options object does not have default value defined");
-
- const input = prompt.querySelector("#promptInput");
- prompt.querySelector("#promptText").innerHTML = promptText;
-
- const type = typeof options.default === "number" ? "number" : "text";
- input.type = type;
-
- if (options.step !== undefined) input.step = options.step;
- if (options.min !== undefined) input.min = options.min;
- if (options.max !== undefined) input.max = options.max;
-
- input.required = options.required === false ? false : true;
- input.placeholder = "type a " + type;
- input.value = options.default;
- input.style.width = promptText.length > 10 ? "100%" : "auto";
- prompt.style.display = "block";
-
- form.addEventListener(
- "submit",
- event => {
- event.preventDefault();
- prompt.style.display = "none";
- const v = type === "number" ? +input.value : input.value;
- if (callback) callback(v);
- },
- {once: true}
- );
- };
-
- const cancel = prompt.querySelector("#promptCancel");
- cancel.addEventListener("click", () => {
- prompt.style.display = "none";
- });
-})();
diff --git a/utils/debugUtils.js b/utils/debugUtils.js
deleted file mode 100644
index 1859f3ae..00000000
--- a/utils/debugUtils.js
+++ /dev/null
@@ -1,72 +0,0 @@
-"use strict";
-// FMG utils used for debugging
-
-function drawCellsValue(data) {
- debug.selectAll("text").remove();
- debug
- .selectAll("text")
- .data(data)
- .enter()
- .append("text")
- .attr("x", (d, i) => pack.cells.p[i][0])
- .attr("y", (d, i) => pack.cells.p[i][1])
- .text(d => d);
-}
-
-function drawPolygons(data) {
- const max = d3.max(data);
- const min = d3.min(data);
- const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
-
- data = data.map(d => 1 - normalize(d, min, max));
-
- debug.selectAll("polygon").remove();
- debug
- .selectAll("polygon")
- .data(data)
- .enter()
- .append("polygon")
- .attr("points", (d, i) => getGridPolygon(i))
- .attr("fill", d => scheme(d))
- .attr("stroke", d => scheme(d));
-}
-
-function drawRouteConnections() {
- debug.select("#connections").remove();
- const routes = debug.append("g").attr("id", "connections").attr("stroke-width", 0.8);
-
- const points = pack.cells.p;
- const links = pack.cells.routes;
-
- for (const from in links) {
- for (const to in links[from]) {
- const [x1, y1] = points[from];
- const [x3, y3] = points[to];
- const [x2, y2] = [(x1 + x3) / 2, (y1 + y3) / 2];
- const routeId = links[from][to];
-
- routes
- .append("line")
- .attr("x1", x1)
- .attr("y1", y1)
- .attr("x2", x2)
- .attr("y2", y2)
- .attr("data-id", routeId)
- .attr("stroke", C_12[routeId % 12]);
- }
- }
-}
-
-function drawPoint([x, y], {color = "red", radius = 0.5}) {
- debug.append("circle").attr("cx", x).attr("cy", y).attr("r", radius).attr("fill", color);
-}
-
-function drawPath(points, {color = "red", width = 0.5}) {
- const lineGen = d3.line().curve(d3.curveBundle);
- debug
- .append("path")
- .attr("d", round(lineGen(points)))
- .attr("stroke", color)
- .attr("stroke-width", width)
- .attr("fill", "none");
-}
diff --git a/utils/functionUtils.js b/utils/functionUtils.js
deleted file mode 100644
index 12570b40..00000000
--- a/utils/functionUtils.js
+++ /dev/null
@@ -1,31 +0,0 @@
-"use strict";
-// FMG helper functions
-
-// extracted d3 code to bypass version conflicts
-// https://github.com/d3/d3-array/blob/main/src/group.js
-function rollups(values, reduce, ...keys) {
- return nest(values, Array.from, reduce, keys);
-}
-
-function nest(values, map, reduce, keys) {
- return (function regroup(values, i) {
- if (i >= keys.length) return reduce(values);
- const groups = new Map();
- const keyof = keys[i++];
- let index = -1;
- for (const value of values) {
- const key = keyof(value, ++index, values);
- const group = groups.get(key);
- if (group) group.push(value);
- else groups.set(key, [value]);
- }
- for (const [key, values] of groups) {
- groups.set(key, regroup(values, i));
- }
- return map(groups);
- })(values, 0);
-}
-
-function dist2([x1, y1], [x2, y2]) {
- return (x1 - x2) ** 2 + (y1 - y2) ** 2;
-}
diff --git a/utils/graphUtils.js b/utils/graphUtils.js
deleted file mode 100644
index 82577b82..00000000
--- a/utils/graphUtils.js
+++ /dev/null
@@ -1,336 +0,0 @@
-"use strict";
-// FMG utils related to graph
-
-// check if new grid graph should be generated or we can use the existing one
-function shouldRegenerateGrid(grid, expectedSeed) {
- if (expectedSeed && expectedSeed !== grid.seed) return true;
-
- const cellsDesired = +byId("pointsInput").dataset.cells;
- 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);
-
- return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
-}
-
-function generateGrid() {
- Math.random = aleaPRNG(seed); // reset PRNG
- const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
- const {cells, vertices} = calculateVoronoi(points, boundary);
- return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed};
-}
-
-// place random points to calculate Voronoi diagram
-function placePoints() {
- TIME && console.time("placePoints");
- const cellsDesired = +byId("pointsInput").dataset.cells;
- const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
-
- const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
- const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
- const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
- const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
- TIME && console.timeEnd("placePoints");
-
- return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
-}
-
-// calculate Delaunay and then Voronoi diagram
-function calculateVoronoi(points, boundary) {
- TIME && console.time("calculateDelaunay");
- const allPoints = points.concat(boundary);
- const delaunay = Delaunator.from(allPoints);
- TIME && console.timeEnd("calculateDelaunay");
-
- TIME && console.time("calculateVoronoi");
- const voronoi = new Voronoi(delaunay, allPoints, points.length);
-
- const cells = voronoi.cells;
- cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i); // array of indexes
- const vertices = voronoi.vertices;
- TIME && console.timeEnd("calculateVoronoi");
-
- return {cells, vertices};
-}
-
-// add points along map edge to pseudo-clip voronoi cells
-function getBoundaryPoints(width, height, spacing) {
- const offset = rn(-1 * spacing);
- const bSpacing = spacing * 2;
- const w = width - offset * 2;
- const h = height - offset * 2;
- const numberX = Math.ceil(w / bSpacing) - 1;
- const numberY = Math.ceil(h / bSpacing) - 1;
- const points = [];
-
- for (let i = 0.5; i < numberX; i++) {
- let 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);
- points.push([offset, y], [w + offset, y]);
- }
-
- return points;
-}
-
-// get points on a regular square grid and jitter them a bit
-function getJitteredGrid(width, height, spacing) {
- 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 = [];
- 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);
- const yj = Math.min(rn(y + jitter(), 2), height);
- points.push([xj, yj]);
- }
- }
- return points;
-}
-
-// return cell index on a regular square grid
-function findGridCell(x, y, grid) {
- return (
- 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
-function findGridAll(x, y, radius) {
- const c = grid.cells.c;
- let r = Math.floor(radius / grid.spacing);
- let found = [findGridCell(x, y, grid)];
- if (!r || radius === 1) return found;
- if (r > 0) found = found.concat(c[found[0]]);
- if (r > 1) {
- let frontier = c[found[0]];
- while (r > 1) {
- let cycle = frontier.slice();
- frontier = [];
- cycle.forEach(function (s) {
- c[s].forEach(function (e) {
- if (found.indexOf(e) !== -1) return;
- found.push(e);
- frontier.push(e);
- });
- });
- r--;
- }
- }
-
- return found;
-}
-
-// return closest pack points quadtree datum
-function find(x, y, radius = Infinity) {
- return pack.cells.q.find(x, y, radius);
-}
-
-// return closest cell index
-function findCell(x, y, radius = Infinity) {
- if (!pack.cells?.q) return;
- const found = pack.cells.q.find(x, y, radius);
- return found ? found[2] : undefined;
-}
-
-// return array of cell indexes in radius
-function findAll(x, y, radius) {
- const found = pack.cells.q.findAll(x, y, radius);
- return found.map(r => r[2]);
-}
-
-// get polygon points for packed cells knowing cell id
-function getPackPolygon(i) {
- return pack.cells.v[i].map(v => pack.vertices.p[v]);
-}
-
-// get polygon points for initial cells knowing cell id
-function getGridPolygon(i) {
- return grid.cells.v[i].map(v => grid.vertices.p[v]);
-}
-
-// mbostock's poissonDiscSampler
-function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
- if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
-
- const width = x1 - x0;
- const height = y1 - y0;
- const r2 = r * r;
- const r2_3 = 3 * r2;
- const cellSize = r * Math.SQRT1_2;
- const gridWidth = Math.ceil(width / cellSize);
- const gridHeight = Math.ceil(height / cellSize);
- const grid = new Array(gridWidth * gridHeight);
- const queue = [];
-
- function far(x, y) {
- const i = (x / cellSize) | 0;
- const j = (y / cellSize) | 0;
- const i0 = Math.max(i - 2, 0);
- const j0 = Math.max(j - 2, 0);
- const i1 = Math.min(i + 3, gridWidth);
- const j1 = Math.min(j + 3, gridHeight);
- for (let j = j0; j < j1; ++j) {
- const o = j * gridWidth;
- for (let i = i0; i < i1; ++i) {
- const s = grid[o + i];
- if (s) {
- const dx = s[0] - x;
- const dy = s[1] - y;
- if (dx * dx + dy * dy < r2) return false;
- }
- }
- }
- return true;
- }
-
- function sample(x, y) {
- queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
- return [x + x0, y + y0];
- }
-
- yield sample(width / 2, height / 2);
-
- pick: while (queue.length) {
- const i = (Math.random() * queue.length) | 0;
- const parent = queue[i];
-
- for (let j = 0; j < k; ++j) {
- const a = 2 * Math.PI * Math.random();
- const r = Math.sqrt(Math.random() * r2_3 + r2);
- const x = parent[0] + r * Math.cos(a);
- const y = parent[1] + r * Math.sin(a);
- if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
- yield sample(x, y);
- continue pick;
- }
- }
-
- const r = queue.pop();
- if (i < queue.length) queue[i] = r;
- }
-}
-
-// filter land cells
-function isLand(i) {
- return pack.cells.h[i] >= 20;
-}
-
-// filter water cells
-function isWater(i) {
- return pack.cells.h[i] < 20;
-}
-
-// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
-void (function addFindAll() {
- const Quad = function (node, x0, y0, x1, y1) {
- this.node = node;
- this.x0 = x0;
- this.y0 = y0;
- this.x1 = x1;
- this.y1 = y1;
- };
-
- const tree_filter = function (x, y, radius) {
- const t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._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++;
-
- // 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
- )
- continue;
-
- // Bisect the current quadrant.
- if (t.node.length) {
- t.node.explored = true;
- var xm = (t.x1 + t.x2) / 2,
- ym = (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)
- );
-
- // Visit the closest quadrant first.
- if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
- 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;
- }
- }
-
- // Visit this point. (Visiting coincident points isn’t necessary!)
- else {
- var dx = x - +this._x.call(null, t.node.data),
- dy = y - +this._y.call(null, t.node.data),
- d2 = dx * dx + dy * dy;
- radiusSearchVisit(t, d2);
- }
- }
- return t.result;
- };
- d3.quadtree.prototype.findAll = tree_filter;
-
- var radiusSearchInit = function (t, radius) {
- t.result = [];
- (t.x0 = t.x - radius), (t.y0 = t.y - radius);
- (t.x3 = t.x + radius), (t.y3 = t.y + radius);
- t.radius = radius * radius;
- };
-
- var radiusSearchVisit = function (t, d2) {
- t.node.data.scanned = true;
- if (d2 < t.radius) {
- do {
- t.result.push(t.node.data);
- t.node.data.selected = true;
- } while ((t.node = t.node.next));
- }
- };
-})();
-
-// draw raster heightmap preview (not used in main generation)
-function drawHeights({heights, width, height, scheme, renderOcean}) {
- 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 => (height < 20 ? (renderOcean ? height : 0) : height);
-
- for (let i = 0; i < heights.length; i++) {
- const color = scheme(1 - getHeight(heights[i]) / 100);
- const {r, g, b} = d3.color(color);
-
- const n = i * 4;
- imageData.data[n] = r;
- imageData.data[n + 1] = g;
- imageData.data[n + 2] = b;
- imageData.data[n + 3] = 255;
- }
-
- ctx.putImageData(imageData, 0, 0);
- return canvas.toDataURL("image/png");
-}
diff --git a/utils/languageUtils.js b/utils/languageUtils.js
deleted file mode 100644
index 9caa1b6f..00000000
--- a/utils/languageUtils.js
+++ /dev/null
@@ -1,174 +0,0 @@
-"use strict";
-
-// chars that serve as vowels
-const VOWELS = `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`;
-function vowel(c) {
- return VOWELS.includes(c);
-}
-
-// remove vowels from the end of the string
-function trimVowels(string, minLength = 3) {
- while (string.length > minLength && vowel(last(string))) {
- string = string.slice(0, -1);
- }
- return string;
-}
-
-const adjectivizationRules = [
- {name: "guo", probability: 1, condition: new RegExp(" Guo$"), action: noun => noun.slice(0, -4)},
- {
- name: "orszag",
- probability: 1,
- condition: new RegExp("orszag$"),
- action: noun => (noun.length < 9 ? noun + "ian" : noun.slice(0, -6))
- },
- {
- name: "stan",
- probability: 1,
- condition: new RegExp("stan$"),
- action: noun => (noun.length < 9 ? noun + "i" : trimVowels(noun.slice(0, -4)))
- },
- {
- name: "land",
- probability: 1,
- condition: new RegExp("land$"),
- action: noun => {
- 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";
- }
- },
- {
- name: "que",
- probability: 1,
- condition: new RegExp("que$"),
- action: noun => noun.replace(/que$/, "can")
- },
- {
- name: "a",
- probability: 1,
- condition: new RegExp("a$"),
- action: noun => noun + "n"
- },
- {
- name: "o",
- probability: 1,
- condition: new RegExp("o$"),
- action: noun => noun.replace(/o$/, "an")
- },
- {
- name: "u",
- probability: 1,
- condition: new RegExp("u$"),
- action: noun => noun + "an"
- },
- {
- name: "i",
- probability: 1,
- condition: new RegExp("i$"),
- action: noun => noun + "an"
- },
- {
- name: "e",
- probability: 1,
- condition: new RegExp("e$"),
- action: noun => noun + "an"
- },
- {
- name: "ay",
- probability: 1,
- condition: new RegExp("ay$"),
- action: noun => noun + "an"
- },
- {
- name: "os",
- probability: 1,
- condition: new RegExp("os$"),
- action: noun => {
- const root = trimVowels(noun.slice(0, -2), 0);
- if (root.length < 4) return noun.slice(0, -1);
- return root + "ian";
- }
- },
- {
- name: "es",
- probability: 1,
- condition: new RegExp("es$"),
- action: noun => {
- const root = trimVowels(noun.slice(0, -2), 0);
- if (root.length > 7) return noun.slice(0, -1);
- return root + "ian";
- }
- },
- {
- name: "l",
- probability: 0.8,
- condition: new RegExp("l$"),
- action: noun => noun + "ese"
- },
- {
- name: "n",
- probability: 0.8,
- condition: new RegExp("n$"),
- action: noun => noun + "ese"
- },
- {
- name: "ad",
- probability: 0.8,
- condition: new RegExp("ad$"),
- action: noun => noun + "ian"
- },
- {
- name: "an",
- probability: 0.8,
- condition: new RegExp("an$"),
- action: noun => noun + "ian"
- },
- {
- name: "ish",
- probability: 0.25,
- condition: new RegExp("^[a-zA-Z]{6}$"),
- action: noun => trimVowels(noun.slice(0, -1)) + "ish"
- },
- {
- name: "an",
- probability: 0.5,
- condition: new RegExp("^[a-zA-Z]{0,7}$"),
- action: noun => trimVowels(noun) + "an"
- }
-];
-
-// get adjective form from noun
-function getAdjective(noun) {
- for (const rule of adjectivizationRules) {
- if (P(rule.probability) && rule.condition.test(noun)) {
- return rule.action(noun);
- }
- }
- return noun; // no rule applied, return noun as is
-}
-
-// get ordinal from integer: 1 => 1st
-const nth = n => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
-
-// get two-letters code (abbreviation) from string
-function abbreviate(name, restricted = []) {
- const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
- const words = parsed.split(" ");
- const letters = words.join("");
-
- 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;
-}
-
-// conjunct array: [A,B,C] => "A, B and C"
-function list(array) {
- if (!Intl.ListFormat) return array.join(", ");
- const conjunction = new Intl.ListFormat(window.lang || "en", {style: "long", type: "conjunction"});
- return conjunction.format(array);
-}
diff --git a/utils/nodeUtils.js b/utils/nodeUtils.js
deleted file mode 100644
index 0010f3d8..00000000
--- a/utils/nodeUtils.js
+++ /dev/null
@@ -1,30 +0,0 @@
-"use strict";
-// FMG utils related to nodes
-
-// remove parent element (usually if child is clicked)
-function removeParent() {
- this.parentNode.parentNode.removeChild(this.parentNode);
-}
-
-// polyfill for composedPath
-function getComposedPath(node) {
- let parent;
- 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];
-}
-
-// get next unused id
-function getNextId(core, i = 1) {
- while (document.getElementById(core + i)) i++;
- return core + i;
-}
-
-function getAbsolutePath(href) {
- if (!href) return "";
- const link = document.createElement("a");
- link.href = href;
- return link.href;
-}
diff --git a/utils/numberUtils.js b/utils/numberUtils.js
deleted file mode 100644
index ada7c284..00000000
--- a/utils/numberUtils.js
+++ /dev/null
@@ -1,26 +0,0 @@
-"use strict";
-// FMG utils related to numbers
-
-// round value to d decimals
-function rn(v, d = 0) {
- const m = Math.pow(10, d);
- return Math.round(v * m) / m;
-}
-
-function minmax(value, min, max) {
- return Math.min(Math.max(value, min), max);
-}
-
-// return value in range [0, 100]
-function lim(v) {
- return minmax(v, 0, 100);
-}
-
-// normalization function
-function normalize(val, min, max) {
- return minmax((val - min) / (max - min), 0, 1);
-}
-
-function lerp(a, b, t) {
- return a + (b - a) * t;
-}
diff --git a/utils/pathUtils.js b/utils/pathUtils.js
deleted file mode 100644
index deafd678..00000000
--- a/utils/pathUtils.js
+++ /dev/null
@@ -1,235 +0,0 @@
-"use strict";
-
-// get continuous paths (isolines) for all cells at once based on getType(cellId) comparison
-function getIsolines(graph, getType, options = {polygons: false, fill: false, halo: false, waterGap: false}) {
- const {cells, vertices} = graph;
- const isolines = {};
-
- const checkedCells = new Uint8Array(cells.i.length);
- const addToChecked = cellId => (checkedCells[cellId] = 1);
- const isChecked = cellId => checkedCells[cellId] === 1;
-
- for (const cellId of cells.i) {
- if (isChecked(cellId) || !getType(cellId)) continue;
- addToChecked(cellId);
-
- const type = getType(cellId);
- const ofSameType = cellId => getType(cellId) === type;
- const ofDifferentType = cellId => getType(cellId) !== type;
-
- const onborderCell = cells.c[cellId].find(ofDifferentType);
- if (onborderCell === undefined) continue;
-
- // 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;
-
- const startingVertex = cells.v[cellId].find(v => 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});
- if (vertexChain.length < 3) continue;
-
- addIsoline(type, vertices, vertexChain);
- }
-
- return isolines;
-
- function addIsoline(type, vertices, vertexChain) {
- 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]));
- }
-
- if (options.fill) {
- if (!isolines[type].fill) isolines[type].fill = "";
- isolines[type].fill += getFillPath(vertices, vertexChain);
- }
-
- if (options.waterGap) {
- if (!isolines[type].waterGap) isolines[type].waterGap = "";
- const isLandVertex = vertexId => vertices.c[vertexId].every(i => cells.h[i] >= 20);
- isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
- }
-
- if (options.halo) {
- if (!isolines[type].halo) isolines[type].halo = "";
- const isBorderVertex = vertexId => vertices.c[vertexId].some(i => cells.b[i]);
- isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
- }
- }
-}
-
-function getFillPath(vertices, vertexChain) {
- const points = vertexChain.map(vertexId => vertices.p[vertexId]);
- const firstPoint = points.shift();
- return `M${firstPoint} L${points.join(" ")} Z`;
-}
-
-function getBorderPath(vertices, vertexChain, discontinue) {
- let discontinued = true;
- let lastOperation = "";
- const path = vertexChain.map(vertexId => {
- if (discontinue(vertexId)) {
- discontinued = true;
- return "";
- }
-
- const operation = discontinued ? "M" : "L";
- discontinued = false;
- lastOperation = operation;
-
- const command = operation === "L" && operation === lastOperation ? "" : operation;
- return ` ${command}${vertices.p[vertexId]}`;
- });
-
- return path.join("").trim();
-}
-
-// get single path for an non-continuous array of cells
-function getVertexPath(cellsArray) {
- const {cells, vertices} = pack;
-
- const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
- const ofSameType = cellId => cellsObj[cellId];
- const ofDifferentType = cellId => !cellsObj[cellId];
-
- const checkedCells = new Uint8Array(cells.c.length);
- const addToChecked = cellId => (checkedCells[cellId] = 1);
- const isChecked = cellId => checkedCells[cellId] === 1;
-
- let path = "";
-
- for (const cellId of cellsArray) {
- if (isChecked(cellId)) continue;
-
- const onborderCell = cells.c[cellId].find(ofDifferentType);
- if (onborderCell === undefined) continue;
-
- const feature = pack.features[cells.f[onborderCell]];
- if (feature.type === "lake" && feature.shoreline) {
- if (feature.shoreline.every(ofSameType)) continue; // inner lake
- }
-
- const startingVertex = cells.v[cellId].find(v => 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});
- if (vertexChain.length < 3) continue;
-
- path += getFillPath(vertices, vertexChain);
- }
-
- return path;
-}
-
-function getPolesOfInaccessibility(graph, getType) {
- const isolines = getIsolines(graph, getType, {polygons: true});
-
- const poles = Object.entries(isolines).map(([id, isoline]) => {
- const multiPolygon = isoline.polygons.sort((a, b) => b.length - a.length);
- const [x, y] = polylabel(multiPolygon, 20);
- return [id, [rn(x), rn(y)]];
- });
-
- return Object.fromEntries(poles);
-}
-
-function connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing}) {
- const MAX_ITERATIONS = vertices.c.length;
- const chain = []; // vertices chain to form a path
-
- let next = startingVertex;
- for (let i = 0; i === 0 || next !== startingVertex; i++) {
- const previous = chain.at(-1);
- const current = next;
- chain.push(current);
-
- const neibCells = vertices.c[current];
- if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
-
- const [c1, c2, c3] = neibCells.map(ofSameType);
- const [v1, v2, v3] = vertices.v[current];
-
- if (v1 !== previous && c1 !== c2) next = v1;
- else if (v2 !== previous && c2 !== c3) next = v2;
- else if (v3 !== previous && c1 !== c3) next = v3;
-
- if (next >= vertices.c.length) {
- ERROR && console.error("ConnectVertices: next vertex is out of bounds");
- break;
- }
-
- if (next === current) {
- ERROR && console.error("ConnectVertices: next vertex is not found");
- break;
- }
-
- if (i === MAX_ITERATIONS) {
- 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.
- * @param {number} start - The ID of the starting cell.
- * @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
- * @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable 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.
- */
-function findPath(start, isExit, getCost) {
- if (isExit(start)) return null;
-
- const from = [];
- const cost = [];
- const queue = new FlatQueue();
- queue.push(start, 0);
-
- while (queue.length) {
- const currentCost = queue.peekValue();
- const current = queue.pop();
-
- for (const next of pack.cells.c[current]) {
- if (isExit(next)) {
- from[next] = current;
- return restorePath(next, start, from);
- }
-
- const nextCost = getCost(current, next);
- if (nextCost === Infinity) continue; // impassable cell
- const totalCost = currentCost + nextCost;
-
- if (totalCost >= cost[next]) continue; // has cheaper path
- from[next] = current;
- cost[next] = totalCost;
- queue.push(next, totalCost);
- }
- }
-
- return null;
-}
-
-// supplementary function for findPath
-function restorePath(exit, start, from) {
- const pathCells = [];
-
- let current = exit;
- let prev = exit;
-
- while (current !== start) {
- pathCells.push(current);
- prev = from[current];
- current = prev;
- }
-
- pathCells.push(current);
-
- return pathCells.reverse();
-}
diff --git a/utils/polyfills.js b/utils/polyfills.js
deleted file mode 100644
index ffc10f74..00000000
--- a/utils/polyfills.js
+++ /dev/null
@@ -1,41 +0,0 @@
-"use strict";
-
-// replaceAll
-if (String.prototype.replaceAll === undefined) {
- String.prototype.replaceAll = function (str, newStr) {
- if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str, newStr);
- return this.replace(new RegExp(str, "g"), newStr);
- };
-}
-
-// flat
-if (Array.prototype.flat === undefined) {
- Array.prototype.flat = function () {
- return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
- };
-}
-
-// at
-if (Array.prototype.at === undefined) {
- Array.prototype.at = function (index) {
- if (index < 0) index += this.length;
- if (index < 0 || index >= this.length) return undefined;
- return this[index];
- };
-}
-
-// readable stream iterator: https://bugs.chromium.org/p/chromium/issues/detail?id=929585#c10
-if (ReadableStream.prototype[Symbol.asyncIterator] === undefined) {
- ReadableStream.prototype[Symbol.asyncIterator] = async function* () {
- const reader = this.getReader();
- try {
- while (true) {
- const {done, value} = await reader.read();
- if (done) return;
- yield value;
- }
- } finally {
- reader.releaseLock();
- }
- };
-}
diff --git a/utils/probabilityUtils.js b/utils/probabilityUtils.js
deleted file mode 100644
index db74c85f..00000000
--- a/utils/probabilityUtils.js
+++ /dev/null
@@ -1,87 +0,0 @@
-"use strict";
-// FMG utils related to randomness
-
-// random number in a range
-function rand(min, max) {
- if (min === undefined && max === undefined) return Math.random();
- if (max === undefined) {
- max = min;
- min = 0;
- }
- return Math.floor(Math.random() * (max - min + 1)) + min;
-}
-
-// probability shorthand
-function P(probability) {
- if (probability >= 1) return true;
- if (probability <= 0) return false;
- return Math.random() < probability;
-}
-
-function each(n) {
- return i => i % n === 0;
-}
-
-/* Random Gaussian number generator
- * @param {number} expected - expected value
- * @param {number} deviation - standard deviation
- * @param {number} min - minimum value
- * @param {number} max - maximum value
- * @param {number} round - round value to n decimals
- * @return {number} random number
- */
-function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
- return rn(minmax(d3.randomNormal(expected, deviation)(), min, max), round);
-}
-
-// probability shorthand for floats
-function Pint(float) {
- return ~~float + +P(float % 1);
-}
-
-// return random value from the array
-function ra(array) {
- return array[Math.floor(Math.random() * array.length)];
-}
-
-// return random value from weighted array {"key1":weight1, "key2":weight2}
-function rw(object) {
- const array = [];
- for (const key in object) {
- for (let i = 0; i < object[key]; i++) {
- array.push(key);
- }
- }
- return array[Math.floor(Math.random() * array.length)];
-}
-
-// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
-function biased(min, max, ex) {
- return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
-}
-
-// get number from string in format "1-3" or "2" or "0.5"
-function getNumberInRange(r) {
- if (typeof r !== "string") {
- ERROR && console.error("Range value should be a string", r);
- return 0;
- }
- if (!isNaN(+r)) return ~~r + +P(r - ~~r);
- const sign = r[0] === "-" ? -1 : 1;
- if (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(range[0] * sign, +range[1]);
- if (isNaN(count) || count < 0) {
- ERROR && console.error("Cannot parse number. Check the format", r);
- return 0;
- }
- return count;
-}
-
-function generateSeed() {
- return String(Math.floor(Math.random() * 1e9));
-}
diff --git a/utils/shorthands.js b/utils/shorthands.js
deleted file mode 100644
index 36e9d09a..00000000
--- a/utils/shorthands.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const byId = document.getElementById.bind(document);
-
-Node.prototype.on = function (name, fn, options) {
- this.addEventListener(name, fn, options);
- return this;
-};
-
-Node.prototype.off = function (name, fn) {
- this.removeEventListener(name, fn);
- return this;
-};
diff --git a/utils/stringUtils.js b/utils/stringUtils.js
deleted file mode 100644
index a3182f14..00000000
--- a/utils/stringUtils.js
+++ /dev/null
@@ -1,81 +0,0 @@
-"use strict";
-// FMG utils related to strings
-
-// round numbers in string to d decimals
-function round(s, d = 1) {
- return s.replace(/[\d\.-][\d\.e-]*/g, function (n) {
- return rn(n, d);
- });
-}
-
-// return string with 1st char capitalized
-function capitalize(string) {
- return string.charAt(0).toUpperCase() + string.slice(1);
-}
-
-// split string into 2 almost equal parts not breaking words
-function splitInTwo(str) {
- const half = str.length / 2;
- const ar = str.split(" ");
- if (ar.length < 2) return ar; // only one word
- let first = "",
- last = "",
- middle = "",
- rest = "";
-
- ar.forEach((w, d) => {
- if (d + 1 !== ar.length) w += " ";
- rest += w;
- if (!first || rest.length < half) first += w;
- else if (!middle) middle = w;
- else last += w;
- });
-
- if (!last) return [first, middle];
- if (first.length < last.length) return [first + middle, last];
- return [first, middle + last];
-}
-
-// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
-function parseTransform(string) {
- if (!string) return [0, 0, 0, 0, 0, 1];
-
- const a = string
- .replace(/[a-z()]/g, "")
- .replace(/[ ]/g, ",")
- .split(",");
- return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
-}
-
-// check if string is a valid for JSON parse
-JSON.isValid = str => {
- try {
- JSON.parse(str);
- return true;
- } catch (e) {
- return false;
- }
-};
-
-JSON.safeParse = str => {
- try {
- return JSON.parse(str);
- } catch (e) {
- return null;
- }
-};
-
-function sanitizeId(string) {
- if (!string) throw new Error("No string provided");
-
- let sanitized = string
- .toLowerCase()
- .trim()
- .replace(/[^a-z0-9-_]/g, "") // no invalid characters
- .replace(/\s+/g, "-"); // replace spaces with hyphens
-
- // remove leading numbers
- if (sanitized.match(/^\d/)) sanitized = "_" + sanitized;
-
- return sanitized;
-}
diff --git a/utils/unitUtils.js b/utils/unitUtils.js
deleted file mode 100644
index d940e349..00000000
--- a/utils/unitUtils.js
+++ /dev/null
@@ -1,37 +0,0 @@
-"use strict";
-// FMG utils related to units
-
-// conver temperature from °C to other scales
-const temperatureConversionMap = {
- "°C": temp => rn(temp) + "°C",
- "°F": temp => rn((temp * 9) / 5 + 32) + "°F",
- K: temp => rn(temp + 273.15) + "K",
- "°R": temp => rn(((temp + 273.15) * 9) / 5) + "°R",
- "°De": temp => rn(((100 - temp) * 3) / 2) + "°De",
- "°N": temp => rn((temp * 33) / 100) + "°N",
- "°Ré": temp => rn((temp * 4) / 5) + "°Ré",
- "°Rø": temp => rn((temp * 21) / 40 + 7.5) + "°Rø"
-};
-
-function convertTemperature(temp, scale = temperatureScale.value || "°C") {
- return temperatureConversionMap[scale](temp);
-}
-
-// corvent number to short string with SI postfix
-function si(n) {
- 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);
-}
-
-// getInteger number from user input data
-function getInteger(value) {
- 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);
-}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..671e6ff5
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,9 @@
+export default {
+ root: './src',
+ base: process.env.NETLIFY ? '/' : '/Fantasy-Map-Generator/',
+ build: {
+ outDir: '../dist',
+ assetsDir: './',
+ },
+ publicDir: '../public',
+}
\ No newline at end of file