Cloud map is used for both land and water. Foreground now uses seeded random number generator for reproducibility of results.master
@@ -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) |
@@ -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", | ||||
@@ -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 | ||||
@@ -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; | ||||
@@ -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; | ||||
} | } | ||||
@@ -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> | ||||
@@ -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, | ||||
@@ -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} | ||||
> | > | ||||
@@ -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, | |||||
} |
@@ -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" | ||||