Browse Source

Add cloud map, random foreground generation

Cloud map is used for both land and water. Foreground now uses seeded
random number generator for reproducibility of results.
master
TheoryOfNekomata 2 years ago
parent
commit
52c74bec13
11 changed files with 221 additions and 77 deletions
  1. +4
    -3
      README.md
  2. +2
    -0
      package.json
  3. BIN
      src/assets/data/000/clouds.png
  4. +24
    -23
      src/backend/services/Biome.service.ts
  5. +4
    -4
      src/backend/services/Projection.service.ts
  6. +90
    -7
      src/backend/services/Tile.service.ts
  7. +11
    -1
      src/components/GenerateMapForm/index.tsx
  8. +20
    -14
      src/pages/api/generate/index.ts
  9. +9
    -23
      src/pages/index.tsx
  10. +47
    -2
      src/utils/types.ts
  11. +10
    -0
      yarn.lock

+ 4
- 3
README.md View File

@@ -44,10 +44,11 @@ This project is made from [Next](https://nextjs.org). Development practices for


Biome map obtained from [Minecraft Earth Map](https://earth.motfe.net/tiles-biomes/). Biome map obtained from [Minecraft Earth Map](https://earth.motfe.net/tiles-biomes/).
Land elevation, bathymetry, and real-color maps obtained from [Visible Earth](https://visibleearth.nasa.gov/). Land elevation, bathymetry, and real-color maps obtained from [Visible Earth](https://visibleearth.nasa.gov/).
Water mask map obtained from [Shaded Relief](https://www.shadedrelief.com/natural3/pages/extra.html).
Uses [D3](https://d3js.org/) (d3-geo) for map projections. Super Mario is a property of Nintendo.
Water mask map obtained from [Shaded Relief](https://www.shadedrelief.com/natural3/pages/extra.html). Cloud map obtained
from [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Asia_Cloud_Map.jpg). Uses [D3](https://d3js.org/)
(d3-geo) for map projections. Super Mario is a property of Nintendo.
[Super Mario War](http://supermariowar.supersanctuary.net/) is a fangame inspired by Super Mario games. [Super Mario War](http://supermariowar.supersanctuary.net/) is a fangame inspired by Super Mario games.


## Samples ## Samples


![Larger Sample Output](./docs/img/sample-output-2.png)
![Larger Sample Output](./docs/img/sample-output-2.png)

+ 2
- 0
package.json View File

@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@theoryofnekomata/formxtra": "^0.2.3", "@theoryofnekomata/formxtra": "^0.2.3",
"chance": "^1.1.8",
"d3": "^7.3.0", "d3": "^7.3.0",
"d3-geo": "^3.0.1", "d3-geo": "^3.0.1",
"d3-geo-polygon": "^1.12.1", "d3-geo-polygon": "^1.12.1",
@@ -22,6 +23,7 @@
"ts-node": "^10.7.0" "ts-node": "^10.7.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chance": "^1.1.3",
"@types/d3": "^7.1.0", "@types/d3": "^7.1.0",
"@types/d3-geo": "^3.0.2", "@types/d3-geo": "^3.0.2",
"@types/node": "17.0.21", "@types/node": "17.0.21",


BIN
src/assets/data/000/clouds.png View File

Before After
Width: 4608  |  Height: 2304  |  Size: 11 MiB

+ 24
- 23
src/backend/services/Biome.service.ts View File

@@ -1,7 +1,9 @@
import ColorService, {ColorServiceImpl} from './Color.service'; import ColorService, {ColorServiceImpl} from './Color.service';
import {LandType} from '../../utils/types';
import {Biome, LandType} from '../../utils/types';


export default interface BiomeService { export default interface BiomeService {
getBiome(x: number, y: number, width: number): Biome
getBiomeByIndex(index: number): Biome
checkBiomeLandType(x: number, y: number, width: number): LandType checkBiomeLandType(x: number, y: number, width: number): LandType
isMountainBiome(x: number, y: number, width: number): boolean isMountainBiome(x: number, y: number, width: number): boolean
isHillBiome(x: number, y: number, width: number): boolean isHillBiome(x: number, y: number, width: number): boolean
@@ -15,13 +17,20 @@ export class BiomeServiceImpl implements BiomeService {
this.colorService = new ColorServiceImpl() this.colorService = new ColorServiceImpl()
} }


checkBiomeLandType(x: number, y: number, width: number): LandType {
const dataIndex = ((y * width) + x) * 4;
switch (this.colorService.rgb(
getBiome(x: number, y: number, width: number): Biome {
return this.getBiomeByIndex(((y * width) + x) * 4);
}

getBiomeByIndex(dataIndex: number): Biome {
return this.colorService.rgb(
this.biomePngData[dataIndex], this.biomePngData[dataIndex],
this.biomePngData[dataIndex + 1], this.biomePngData[dataIndex + 1],
this.biomePngData[dataIndex + 2], this.biomePngData[dataIndex + 2],
)) {
);
}

checkBiomeLandType(x: number, y: number, width: number): LandType {
switch (this.getBiome(x, y, width)) {
case 0xfa9418: case 0xfa9418:
case 0xffbc40: case 0xffbc40:
return LandType.DESERT; return LandType.DESERT;
@@ -75,12 +84,7 @@ export class BiomeServiceImpl implements BiomeService {
} }


isMountainBiome(x: number, y: number, width: number): boolean { isMountainBiome(x: number, y: number, width: number): boolean {
const dataIndex = ((y * width) + x) * 4;
switch (this.colorService.rgb(
(this.biomePngData[dataIndex]),
(this.biomePngData[dataIndex + 1]),
(this.biomePngData[dataIndex + 2]),
)) {
switch (this.getBiome(x, y, width)) {
case 0x606060: case 0x606060:
case 0xa0a0a0: case 0xa0a0a0:
case 0x507050: case 0x507050:
@@ -95,17 +99,14 @@ export class BiomeServiceImpl implements BiomeService {
} }


isHillBiome(x: number, y: number, width: number): boolean { isHillBiome(x: number, y: number, width: number): boolean {
const dataIndex = ((y * width) + x) * 4;
switch (this.colorService.rgb(
(this.biomePngData[dataIndex]),
(this.biomePngData[dataIndex + 1]),
(this.biomePngData[dataIndex + 2]),
)) {
switch (this.getBiome(x, y, width)) {
case 0xffffff:
case 0xd25f12: case 0xd25f12:
case 0x163933: case 0x163933:
case 0x2c4205: case 0x2c4205:
case 0x454f3e: case 0x454f3e:
case 0x687942: case 0x687942:
case Biome.SAVANNA:
return true return true
default: default:
break break
@@ -115,16 +116,16 @@ export class BiomeServiceImpl implements BiomeService {
} }


isWoodedBiome(x: number, y: number, width: number): boolean { isWoodedBiome(x: number, y: number, width: number): boolean {
const dataIndex = ((y * width) + x) * 4;
switch (this.colorService.rgb(
(this.biomePngData[dataIndex]),
(this.biomePngData[dataIndex + 1]),
(this.biomePngData[dataIndex + 2]),
)) {
switch (this.getBiome(x, y, width)) {
case 0x0b6659:
case 0x8db360:
case 0x056621: case 0x056621:
case 0x537b09: case 0x537b09:
case 0x596651: case 0x596651:
case 0x2d8e49: case 0x2d8e49:
case 0x628b17:
case Biome.DESERT:
case Biome.GIANT_TREE_TAIGA:
return true; return true;
default: default:
break break


+ 4
- 4
src/backend/services/Projection.service.ts View File

@@ -1,5 +1,5 @@
import {PNG} from 'pngjs'; import {PNG} from 'pngjs';
import {Projection} from '../../utils/types';
import {Bounds, Projection} from '../../utils/types';
import * as d3geo from 'd3-geo'; import * as d3geo from 'd3-geo';


type ProjectionData = { type ProjectionData = {
@@ -8,7 +8,7 @@ type ProjectionData = {
} }


export default interface ProjectionService { export default interface ProjectionService {
project(equiImage: PNG, projection: Projection): PNG | null
project(equiImage: PNG, projection: Projection, bounds: Bounds): PNG | null
} }


export class ProjectionServiceImpl implements ProjectionService { export class ProjectionServiceImpl implements ProjectionService {
@@ -33,14 +33,14 @@ export class ProjectionServiceImpl implements ProjectionService {


private readonly referenceWidth = 960 as const; private readonly referenceWidth = 960 as const;


project(equiImage: PNG, projection: Projection): PNG | null {
project(equiImage: PNG, projection: Projection, bounds: Bounds): PNG | null {
if (projection === Projection.EQUIRECTANGULAR) { if (projection === Projection.EQUIRECTANGULAR) {
return equiImage; return equiImage;
} }


const { [projection]: currentProjectionData } = this.projections const { [projection]: currentProjectionData } = this.projections
const baseProjection = currentProjectionData.fn(); const baseProjection = currentProjectionData.fn();
const modifiedProjection = baseProjection.center([0, 0]);
const modifiedProjection = baseProjection.center([(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]);
const { invert } = modifiedProjection; const { invert } = modifiedProjection;
if (!invert) { if (!invert) {
return null; return null;


+ 90
- 7
src/backend/services/Tile.service.ts View File

@@ -1,5 +1,7 @@
import BiomeService, {BiomeServiceImpl} from './Biome.service'; import BiomeService, {BiomeServiceImpl} from './Biome.service';
import {LandType, WaterType} from '../../utils/types';
import {Biome, LandType, WaterType} from '../../utils/types';
import Chance from 'chance';
import ColorService, {ColorServiceImpl} from './Color.service';


export type NeighborData = { export type NeighborData = {
nw: boolean, nw: boolean,
@@ -20,6 +22,9 @@ export default interface TileService {
} }


export class TileServiceImpl implements TileService { export class TileServiceImpl implements TileService {
private readonly rng: Chance.Chance;
private readonly colorService: ColorService;

private index(x: number, y: number, width: number) { private index(x: number, y: number, width: number) {
return ((y * width) + x) * 4; return ((y * width) + x) * 4;
} }
@@ -199,27 +204,91 @@ export class TileServiceImpl implements TileService {
switch (landType) { switch (landType) {
case LandType.DESERT: case LandType.DESERT:
case LandType.MESA: case LandType.MESA:
if (!this.rng.bool({ likelihood: 80 })) {
return 0
}
return 796; return 796;
case LandType.ICE: case LandType.ICE:
if (!this.rng.bool({ likelihood: 20 })) {
return 0
}
return 799; return 799;
case LandType.FOREST: case LandType.FOREST:
case LandType.PLAINS: case LandType.PLAINS:
default: default:
return 797;
break
} }
if (!this.rng.bool({ likelihood: 80 })) {
return 0
}
return 797;

} }


private getHillForegroundTileType(landType: LandType): number {
private getHillForegroundTileType(landType: LandType, index: number): number {
if (landType === LandType.ICE) { if (landType === LandType.ICE) {
if (!this.rng.bool({ likelihood: 30 })) {
return 0
}
return 903 return 903
} }

switch(this.biomeService.getBiomeByIndex(index)) {
case Biome.SAVANNA:
if (!this.rng.bool({ likelihood: 30 })) {
return 0
}
return this.rng.bool() ? 901 : 902;
default:
break;
}

if (!this.rng.bool({ likelihood: 70 })) {
return 0
}
return 901 return 901
} }


private getWoodedForegroundTileType(landType: LandType): number {
private getWoodedForegroundTileType(landType: LandType, index: number): number {
if (landType === LandType.DESERT) {
if (this.rng.bool({ likelihood: 5 })) {
return 904;
}
return 0;
}

if (landType === LandType.ICE) { if (landType === LandType.ICE) {
if (!this.rng.bool({ likelihood: 30 })) {
return 0
}
return 906 return 906
} }

switch (this.biomeService.getBiomeByIndex(index)) {
case Biome.JUNGLE:
case Biome.JUNGLE_EDGE:
if (!this.rng.bool({ likelihood: 60 })) {
return 0
}
return 904;
case Biome.FOREST:
if (!this.rng.bool({ likelihood: 60 })) {
return 0
}
return 907;
case Biome.TAIGA:
case Biome.GIANT_TREE_TAIGA:
if (!this.rng.bool({ likelihood: 20 })) {
return 0
}
return 908;
default:
break;
}

if (!this.rng.bool({ likelihood: 50 })) {
return 0
}
return 905 return 905
} }


@@ -228,8 +297,11 @@ export class TileServiceImpl implements TileService {
private readonly elevaionPngData: Buffer, private readonly elevaionPngData: Buffer,
private readonly waterMaskPngData: Buffer, private readonly waterMaskPngData: Buffer,
private readonly bathymetryPngData: Buffer, private readonly bathymetryPngData: Buffer,
private readonly cloudsPngData: Buffer,
) { ) {
this.biomeService = new BiomeServiceImpl(biomePngData) this.biomeService = new BiomeServiceImpl(biomePngData)
this.colorService = new ColorServiceImpl()
this.rng = Chance(69420);
} }


determineBackgroundTileType(x: number, y: number, width: number, height: number): number { determineBackgroundTileType(x: number, y: number, width: number, height: number): number {
@@ -239,9 +311,14 @@ export class TileServiceImpl implements TileService {
} }


private determineLandForegroundTileType(x: number, y: number, dx: number): number { private determineLandForegroundTileType(x: number, y: number, dx: number): number {
const i = this.index(x, y, dx);

if (this.cloudsPngData[i] >= 0x20 && this.rng.bool({ likelihood: 10 })) {
return 790;
}

const landType = this.biomeService.checkBiomeLandType(x, y, dx); const landType = this.biomeService.checkBiomeLandType(x, y, dx);


const i = this.index(x, y, dx);
const isMountain = this.elevaionPngData[i] >= 0x40 || this.biomeService.isMountainBiome( const isMountain = this.elevaionPngData[i] >= 0x40 || this.biomeService.isMountainBiome(
x, x,
y, y,
@@ -252,17 +329,23 @@ export class TileServiceImpl implements TileService {
} }


if (this.biomeService.isHillBiome(x, y, dx)) { if (this.biomeService.isHillBiome(x, y, dx)) {
return this.getHillForegroundTileType(landType)
return this.getHillForegroundTileType(landType, i);
} }


if (this.biomeService.isWoodedBiome(x, y, dx)) { if (this.biomeService.isWoodedBiome(x, y, dx)) {
return this.getWoodedForegroundTileType(landType);
return this.getWoodedForegroundTileType(landType, i);
} }


return 0; return 0;
} }


private determineWaterForegroundTileType(x: number, y: number, dx: number): number { private determineWaterForegroundTileType(x: number, y: number, dx: number): number {
const i = this.index(x, y, dx);

if (this.cloudsPngData[i] >= 0x20 && this.rng.bool({ likelihood: 20 })) {
return 790;
}

return 0; return 0;
} }




+ 11
- 1
src/components/GenerateMapForm/index.tsx View File

@@ -5,7 +5,13 @@ import {NumericInput} from '../NumericInput';
import styles from '../../styles/components/GenerateMapForm/index.module.css'; import styles from '../../styles/components/GenerateMapForm/index.module.css';
import {ActionButton} from '../ActionButton'; import {ActionButton} from '../ActionButton';


export const GenerateMapForm: VFC = () => {
type GenerateMapFormProps = {
disabled?: boolean;
}

export const GenerateMapForm: VFC<GenerateMapFormProps> = ({
disabled = false,
}) => {
return ( return (
<div <div
className={styles.base} className={styles.base}
@@ -15,6 +21,7 @@ export const GenerateMapForm: VFC = () => {
> >
<fieldset <fieldset
className={styles.fieldset} className={styles.fieldset}
disabled={disabled}
> >
<legend> <legend>
Base Base
@@ -150,6 +157,7 @@ export const GenerateMapForm: VFC = () => {
> >
<fieldset <fieldset
className={styles.fieldset} className={styles.fieldset}
disabled={disabled}
> >
<legend> <legend>
Cities Cities
@@ -162,6 +170,7 @@ export const GenerateMapForm: VFC = () => {
> >
<fieldset <fieldset
className={styles.fieldset} className={styles.fieldset}
disabled={disabled}
> >
<legend> <legend>
Map Map
@@ -206,6 +215,7 @@ export const GenerateMapForm: VFC = () => {
> >
<ActionButton <ActionButton
block block
disabled={disabled}
> >
Generate Map Generate Map
</ActionButton> </ActionButton>


+ 20
- 14
src/pages/api/generate/index.ts View File

@@ -1,15 +1,16 @@
import {NextApiHandler} from 'next'; import {NextApiHandler} from 'next';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import {PNG} from 'pngjs'; import {PNG} from 'pngjs';
import {MapType, Projection, WorldData} from '../../../utils/types';
import {Bounds, MapType, Projection, WorldData} from '../../../utils/types';
import {Stats} from 'fs'; import {Stats} from 'fs';
import sharp from 'sharp'; import sharp from 'sharp';
import ProjectionService, {ProjectionServiceImpl} from '../../../backend/services/Projection.service'; import ProjectionService, {ProjectionServiceImpl} from '../../../backend/services/Projection.service';
import {FileSystemServiceImpl} from '../../../backend/services/FileSystem.service'; import {FileSystemServiceImpl} from '../../../backend/services/FileSystem.service';
import TileService, {TileServiceImpl} from '../../../backend/services/Tile.service'; import TileService, {TileServiceImpl} from '../../../backend/services/Tile.service';


const generateProjectedBaseData = async (t: MapType, p: Projection) => {
const destPath = `public/generated/base/${p}-${t}.png`;
const stringifyBounds = (bounds: Bounds) => bounds.map(b => b.map(c => Math.floor(c).toString()).join('_')).join('_');
const generateProjectedBaseData = async (t: MapType, p: Projection, bounds: Bounds) => {
const destPath = `public/generated/base/${p}_${t}_${stringifyBounds(bounds)}.png`;
let stat: Stats | undefined; let stat: Stats | undefined;
let shouldGenerateFile = false; let shouldGenerateFile = false;
try { try {
@@ -27,7 +28,7 @@ const generateProjectedBaseData = async (t: MapType, p: Projection) => {
const inputBuffer = await fs.readFile(`src/assets/data/000/${t}.png`) const inputBuffer = await fs.readFile(`src/assets/data/000/${t}.png`)
const inputPng = PNG.sync.read(inputBuffer); const inputPng = PNG.sync.read(inputBuffer);
const projectionService: ProjectionService = new ProjectionServiceImpl() const projectionService: ProjectionService = new ProjectionServiceImpl()
const outputPng = projectionService.project(inputPng, p) as PNG;
const outputPng = projectionService.project(inputPng, p, bounds) as PNG;
const outputBuffer = PNG.sync.write(outputPng); const outputBuffer = PNG.sync.write(outputPng);
await fs.writeFile(destPath, outputBuffer); await fs.writeFile(destPath, outputBuffer);
} }
@@ -39,6 +40,7 @@ const getResizeKernel = (t: MapType) => {
switch (t) { switch (t) {
case MapType.BIOME: case MapType.BIOME:
case MapType.BATHYMETRY: case MapType.BATHYMETRY:
case MapType.CLOUDS:
return 'nearest'; return 'nearest';
case MapType.WATER_MASK: case MapType.WATER_MASK:
return 'lanczos2'; return 'lanczos2';
@@ -48,9 +50,9 @@ const getResizeKernel = (t: MapType) => {
return 'cubic'; return 'cubic';
} }


const resizeBaseData = async (t: MapType, p: Projection, size: number, isWidthDimension: boolean) => {
const destPath = `public/generated/resized/${isWidthDimension ? 'w' : 'h'}-${size}-${p}-${t}.png`;
let resizeChain = sharp(`public/generated/base/${p}-${t}.png`)
const resizeBaseData = async (t: MapType, p: Projection, bounds: Bounds, size: number, isWidthDimension: boolean) => {
const destPath = `public/generated/resized/${isWidthDimension ? 'w' : 'h'}_${size}_${p}_${t}_${stringifyBounds(bounds)}.png`;
let resizeChain = sharp(`public/generated/base/${p}_${t}_${stringifyBounds(bounds)}.png`)
.resize({ .resize({
[isWidthDimension ? 'width' : 'height']: size, [isWidthDimension ? 'width' : 'height']: size,
kernel: getResizeKernel(t), kernel: getResizeKernel(t),
@@ -68,18 +70,18 @@ const resizeBaseData = async (t: MapType, p: Projection, size: number, isWidthDi
return destPath; return destPath;
} }


const generateWorldData = async (p: Projection, size: number, isWidthDimension: boolean) => {
const generateWorldData = async (p: Projection, bounds: Bounds, size: number, isWidthDimension: boolean) => {
await FileSystemServiceImpl.ensureDirectory('public/generated/resized'); await FileSystemServiceImpl.ensureDirectory('public/generated/resized');
const tempResizedFiles = await Promise.all( const tempResizedFiles = await Promise.all(
Object Object
.values(MapType) .values(MapType)
.map(async (t: MapType) => resizeBaseData(t, p, size, isWidthDimension))
.map(async (t: MapType) => resizeBaseData(t, p, bounds, size, isWidthDimension))
); );


const mapTypeBuffers = await Promise.all(Object.values(MapType).map(async (t: MapType) => { const mapTypeBuffers = await Promise.all(Object.values(MapType).map(async (t: MapType) => {
return { return {
mapType: t, mapType: t,
buffer: await fs.readFile(`public/generated/resized/${isWidthDimension ? 'w' : 'h'}-${size}-${p}-${t}.png`),
buffer: await fs.readFile(`public/generated/resized/${isWidthDimension ? 'w' : 'h'}_${size}_${p}_${t}_${stringifyBounds(bounds)}.png`),
}; };
})) }))


@@ -116,7 +118,8 @@ const generateWorldData = async (p: Projection, size: number, isWidthDimension:
data[MapType.BIOME].data, data[MapType.BIOME].data,
data[MapType.ELEVATION].data, data[MapType.ELEVATION].data,
data[MapType.WATER_MASK].data, data[MapType.WATER_MASK].data,
data[MapType.BATHYMETRY].data
data[MapType.BATHYMETRY].data,
data[MapType.CLOUDS].data,
) )


let i = 0; let i = 0;
@@ -165,21 +168,24 @@ const generateHandler: NextApiHandler = async (req, res) => {
projection = Projection.EQUIRECTANGULAR, projection = Projection.EQUIRECTANGULAR,
width, width,
height, height,
bounds = '-180,90;180,-90',
bounds: rawBounds = '-180,90;180,-90',
} = req.query } = req.query
const { size, isWidthDimension } = determineDimension( const { size, isWidthDimension } = determineDimension(
Array.isArray(width) ? width[0] : width, Array.isArray(width) ? width[0] : width,
Array.isArray(height) ? height[0] : height, Array.isArray(height) ? height[0] : height,
); );


const bounds = rawBounds as string;
const boundsParsed = bounds.split(';').map((b) => b.split(',').map(c => Number(c))) as Bounds;

await ensureDirectories(); await ensureDirectories();
const baseDataImageUrls = await Promise.all( const baseDataImageUrls = await Promise.all(
Object Object
.values(MapType) .values(MapType)
.map(async (t: MapType) => generateProjectedBaseData(t, projection as Projection))
.map(async (t: MapType) => generateProjectedBaseData(t, projection as Projection, boundsParsed))
); );


const worldData = await generateWorldData(projection as Projection, size, isWidthDimension);
const worldData = await generateWorldData(projection as Projection, boundsParsed, size, isWidthDimension);


res.json({ res.json({
baseDataImageUrls, baseDataImageUrls,


+ 9
- 23
src/pages/index.tsx View File

@@ -18,22 +18,6 @@ import WORLD_FOREGROUND from '../assets/gfx/world_foreground.png';
import {DropdownSelect} from '../components/DropdownSelect'; import {DropdownSelect} from '../components/DropdownSelect';
import {ActionButton} from '../components/ActionButton'; import {ActionButton} from '../components/ActionButton';


const IMAGE_URLS_INDEX = [
MapType.BIOME,
MapType.ELEVATION,
MapType.WATER_MASK,
MapType.BATHYMETRY,
MapType.REAL_COLOR,
]

const IMAGE_STACKING = [
MapType.WATER_MASK,
MapType.BATHYMETRY,
MapType.ELEVATION,
MapType.REAL_COLOR,
MapType.BIOME,
]

const loadImage = async (src: string, processTransparency = false) => { const loadImage = async (src: string, processTransparency = false) => {
return new Promise<HTMLImageElement>((resolve, reject) => { return new Promise<HTMLImageElement>((resolve, reject) => {
const originalImage = new Image() const originalImage = new Image()
@@ -235,8 +219,9 @@ ${new Array(worldData.height).fill(new Array(worldData.width).fill('0').join(','


const IndexPage: NextPage = () => { const IndexPage: NextPage = () => {
const [data, setData] = useState<Data>() const [data, setData] = useState<Data>()
const [loading, setLoading] = useState(false)
const [previewMapOpacity, setPreviewMapOpacity] = useState<Record<MapType, number>>(() => { const [previewMapOpacity, setPreviewMapOpacity] = useState<Record<MapType, number>>(() => {
return IMAGE_URLS_INDEX.reduce(
return Object.values(MapType).reduce(
(theValue, mapType) => { (theValue, mapType) => {
return { return {
...theValue, ...theValue,
@@ -252,11 +237,11 @@ const IndexPage: NextPage = () => {
const scrollRef = useRef(false) const scrollRef = useRef(false)
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => { const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault() e.preventDefault()
const form = e.target as HTMLFormElement
const values = getFormValues(form)
const values = getFormValues(e.currentTarget)
const url = new URL('/api/generate', 'http://localhost:3000'); const url = new URL('/api/generate', 'http://localhost:3000');
const search = new URLSearchParams(values) const search = new URLSearchParams(values)
url.search = search.toString() url.search = search.toString()
setLoading(true)
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -267,6 +252,7 @@ const IndexPage: NextPage = () => {
if (response.ok) { if (response.ok) {
setData(responseData) setData(responseData)
} }
setLoading(false)
} }


const saveScreenshot: MouseEventHandler = async (e) => { const saveScreenshot: MouseEventHandler = async (e) => {
@@ -373,7 +359,7 @@ const IndexPage: NextPage = () => {
className={styles.form} className={styles.form}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<GenerateMapForm />
<GenerateMapForm disabled={loading} />
</form> </form>
</div> </div>
<div <div
@@ -389,10 +375,10 @@ const IndexPage: NextPage = () => {
ref={baseMapScrollRef} ref={baseMapScrollRef}
> >
{ {
IMAGE_STACKING.map((v: MapType) => (
Object.values(MapType).map((v: MapType, index: number) => (
<img <img
key={v} key={v}
src={data.baseDataImageUrls[IMAGE_URLS_INDEX.indexOf(v)]}
src={data.baseDataImageUrls[index]}
alt={v} alt={v}
style={{ style={{
opacity: previewMapOpacity[v], opacity: previewMapOpacity[v],
@@ -406,7 +392,7 @@ const IndexPage: NextPage = () => {
className={styles.mapForm} className={styles.mapForm}
> >
{ {
IMAGE_STACKING.map((v: MapType) => (
Object.values(MapType).map((v: MapType) => (
<div <div
key={v} key={v}
> >


+ 47
- 2
src/utils/types.ts View File

@@ -6,10 +6,11 @@ export enum Projection {
} }


export enum MapType { export enum MapType {
BIOME = 'biomes',
ELEVATION = 'elevation',
WATER_MASK = 'water-mask', WATER_MASK = 'water-mask',
BATHYMETRY = 'bathymetry', BATHYMETRY = 'bathymetry',
ELEVATION = 'elevation',
CLOUDS = 'clouds',
BIOME = 'biomes',
REAL_COLOR = 'real-color', REAL_COLOR = 'real-color',
} }


@@ -45,3 +46,47 @@ export enum LandType {
BEACH = 5, BEACH = 5,
MUD = 6, MUD = 6,
} }

export type Coords = [number, number];
export type Bounds = [Coords, Coords];

export enum Biome {
OCEAN = 0x000070,
PLAINS = 0x8db360,
DESERT = 0xfa9418,
MOUNTAINS = 0x606060,
FOREST = 0x056621,
TAIGA = 0x0b6659,
SWAMP = 0x07f9b2,
RIVER = 0x0000ff,
SNOWY_TUNDRA = 0xffffff,
SNOWY_MOUNTAINS = 0xa0a0a0,
BEACH = 0xfade55,
DESERT_HILLS = 0xd25f12,
TAIGA_HILLS = 0x163933,
JUNGLE = 0x537b09,
JUNGLE_HILLS = 0x2c4205,
JUNGLE_EDGE = 0x628b17,
SNOWY_BEACH = 0xfaf0c0,
GIANT_TREE_TAIGA = 0x596651,
GIANT_TREE_TAIGA_HILLS = 0x454f3e,
WOODED_MOUNTAINS = 0x507050,
SAVANNA = 0xbdb25f,
SAVANNA_PLATEAU = 0xa79d64,
BADLANDS = 0xd94515,
BADLANDS_PLATEAU = 0xca8c65,
WARM_OCEAN = 0x0000ac,
LUKEWARM_OCEAN = 0x000090,
COLD_OCEAN = 0x202070,
DEEP_WARM_OCEAN = 0x000050,
DEEP_LUKEWARM_OCEAN = 0x000040,
DEEP_COLD_OCEAN = 0x202038,
DEEP_FROZEN_OCEAN = 0x404090,
SUNFLOWER_PLAINS = 0xb5db88,
DESERT_LAKES = 0xffbc40,
FLOWER_FOREST = 0x2d8e49,
TAIGA_MOUNTAINS = 0x338e81,
MODIFIED_JUNGLE_EDGE = 0x8ab33f,
DARK_FOREST_HILLS = 0x687942,
SNOWY_TAIGA_MOUNTAINS = 0x597d72,
}

+ 10
- 0
yarn.lock View File

@@ -176,6 +176,11 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
"@types/chance@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.3.tgz#d19fe9391288d60fdccd87632bfc9ab2b4523fea"
integrity sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==
"@types/d3-array@*": "@types/d3-array@*":
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.2.tgz#71c35bca8366a40d1b8fce9279afa4a77fb0065d" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.2.tgz#71c35bca8366a40d1b8fce9279afa4a77fb0065d"
@@ -668,6 +673,11 @@ chalk@^4.0.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chance@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.8.tgz#5d6c2b78c9170bf6eb9df7acdda04363085be909"
integrity sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg==
chownr@^1.1.1: chownr@^1.1.1:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"


Loading…
Cancel
Save