Merge branch 'master' of github.com-personal:Azgaar/Fantasy-Map-Generator into overview-search
89
.github/copilot-instructions.md
vendored
|
|
@ -1,58 +1,76 @@
|
||||||
# Fantasy Map Generator
|
# 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.
|
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
|
## Working Effectively
|
||||||
|
|
||||||
- **CRITICAL**: This is a static web application - NO build process needed. No npm install, no compilation, no bundling.
|
- The project uses NPM, Vite, and TypeScript for development and building.
|
||||||
- Run the application using HTTP server (required - cannot run with file:// protocol):
|
- **Setup**: Run `npm install` to install dependencies (requires Node.js >= 24.0.0)
|
||||||
- `python3 -m http.server 8000` - takes 2-3 seconds to start
|
- **Development**: Run `npm run dev` to start the Vite development server
|
||||||
- Access at: `http://localhost:8000`
|
- 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
|
## Validation
|
||||||
|
|
||||||
- Always manually validate any changes by:
|
- Always manually validate any changes by:
|
||||||
1. Starting the HTTP server (NEVER CANCEL - wait for full startup)
|
1. Run `npm run dev` to start the development server (wait for "ready" message)
|
||||||
2. Navigate to the application in browser
|
2. Navigate to the application in browser (typically `http://localhost:5173`)
|
||||||
3. Click the "►" button to open the menu and generate a new map
|
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
|
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
|
5. Test UI interaction: click "Layers" button, verify layer controls work
|
||||||
6. Test regeneration: click "New Map!" button, verify new map generates correctly
|
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)
|
- **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
|
## Repository Structure
|
||||||
|
|
||||||
### Core Files
|
### Core Files
|
||||||
|
|
||||||
- `index.html` - Main application entry point
|
- `package.json` - NPM package configuration with scripts and dependencies
|
||||||
- `main.js` - Core application logic
|
- `vite.config.ts` - Vite build configuration
|
||||||
- `versioning.js` - Version management and update handling
|
- `tsconfig.json` - TypeScript compiler configuration
|
||||||
|
|
||||||
### Key Directories
|
### Source Directories
|
||||||
|
|
||||||
- `modules/` - core functionality modules:
|
- `src/` - Source code directory (build input)
|
||||||
- `modules/ui/` - UI components (editors, tools, style management)
|
- `src/index.html` - Main application entry point
|
||||||
- `modules/dynamic/` - runtime modules (export, installation)
|
- `src/utils/` - TypeScript utility modules (migrated from JS)
|
||||||
- `modules/renderers/` - drawing and rendering logic
|
- `public/` - Static assets (copied to build output)
|
||||||
- `utils/` - utility libraries (math, arrays, strings, etc.)
|
- `public/main.js` - Core application logic
|
||||||
- `styles/` - visual style presets (JSON files)
|
- `public/versioning.js` - Version management and update handling
|
||||||
- `libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
|
- `public/modules/` - Core functionality modules:
|
||||||
- `images/` - backgrounds, UI elements
|
- `modules/ui/` - UI components (editors, tools, style management)
|
||||||
- `charges/` - heraldic symbols and coat of arms elements
|
- `modules/dynamic/` - runtime modules (export, installation)
|
||||||
- `config/` - Heightmap templates and configurations
|
- `modules/renderers/` - drawing and rendering logic
|
||||||
- `heightmaps/` - Terrain generation data
|
- `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
|
## Common Tasks
|
||||||
|
|
||||||
### Making Code Changes
|
### Making Code Changes
|
||||||
|
|
||||||
1. Edit JavaScript files directly (no compilation needed)
|
1. **TypeScript files** (`src/utils/*.ts`):
|
||||||
2. Refresh browser to see changes immediately
|
- Edit TypeScript files in the `src/utils/` directory
|
||||||
3. **ALWAYS test map generation** after making changes
|
- Changes are automatically recompiled and hot-reloaded in dev mode
|
||||||
4. Update version in `versioning.js` for all changes
|
- Run `npm run build` to verify TypeScript compilation succeeds
|
||||||
5. Update file hashes in `index.html` for changed files (format: `file.js?v=1.108.1`)
|
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
|
### Debugging Map Generation
|
||||||
|
|
||||||
|
|
@ -71,19 +89,30 @@ Always reference these instructions first and fallback to search or bash command
|
||||||
|
|
||||||
### Application Won't Load
|
### Application Won't Load
|
||||||
|
|
||||||
- Ensure using HTTP server (not file://)
|
- Run `npm install` to ensure dependencies are installed
|
||||||
- Check console for JavaScript errors
|
- Run `npm run dev` to start the development server
|
||||||
|
- Check console for JavaScript/TypeScript errors
|
||||||
- Verify all files are present in repository
|
- 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
|
### Map Generation Fails
|
||||||
|
|
||||||
- Check browser console for error messages
|
- Check browser console for error messages
|
||||||
- Look for specific module failures in generation logs
|
- Look for specific module failures in generation logs
|
||||||
- Try refreshing page and generating new map
|
- Try refreshing page and generating new map
|
||||||
|
- Verify build completed successfully if using production build
|
||||||
|
|
||||||
### Performance Issues
|
### Performance Issues
|
||||||
|
|
||||||
- Map generation should complete in ~1 second for standard configurations
|
- Map generation should complete in ~1 second for standard configurations
|
||||||
- If slower, check browser console for errors
|
- 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.
|
||||||
|
|
|
||||||
51
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -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
|
||||||
11
.vscode/launch.json
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.1.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Debug",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"file": "${workspaceFolder}/index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
28
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
|
FROM nginx:stable-alpine
|
||||||
|
|
||||||
# Copy the contents of the repo to the container
|
# Copy built files from builder stage
|
||||||
COPY . /usr/share/nginx/html
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# Move the customized nginx config file to the nginx folder
|
# Copy 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 .docker/default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
9
netlify.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[build]
|
||||||
|
command = "npm run build"
|
||||||
|
publish = "dist"
|
||||||
|
environment = { NODE_VERSION = "24" }
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/index.html"
|
||||||
|
status = 200
|
||||||
1916
package-lock.json
generated
Normal file
37
package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 415 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 931 B After Width: | Height: | Size: 931 B |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 268 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 898 B After Width: | Height: | Size: 898 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 9 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 372 B After Width: | Height: | Size: 372 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 386 B |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 447 B After Width: | Height: | Size: 447 B |
|
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 532 B After Width: | Height: | Size: 532 B |
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 367 B |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 553 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 608 B |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 429 B |
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 484 B |
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 300 B |
|
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 633 B |
|
Before Width: | Height: | Size: 320 B After Width: | Height: | Size: 320 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 354 B |