Преглед на файлове

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 години
родител
ревизия
52c74bec13
променени са 11 файла, в които са добавени 221 реда и са изтрити 77 реда
  1. +4
    -3
      README.md
  2. +2
    -0
      package.json
  3. Двоични данни
      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 Целия файл

@@ -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/).
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.

## Samples

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

+ 2
- 0
package.json Целия файл

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


Двоични данни
src/assets/data/000/clouds.png Целия файл

Преди След
Ширина: 4608  |  Височина: 2304  |  Големина: 11 MiB

+ 24
- 23
src/backend/services/Biome.service.ts Целия файл

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

export default interface BiomeService {
getBiome(x: number, y: number, width: number): Biome
getBiomeByIndex(index: number): Biome
checkBiomeLandType(x: number, y: number, width: number): LandType
isMountainBiome(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()
}

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 + 1],
this.biomePngData[dataIndex + 2],
)) {
);
}

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

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 0xa0a0a0:
case 0x507050:
@@ -95,17 +99,14 @@ export class BiomeServiceImpl implements BiomeService {
}

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 0x163933:
case 0x2c4205:
case 0x454f3e:
case 0x687942:
case Biome.SAVANNA:
return true
default:
break
@@ -115,16 +116,16 @@ export class BiomeServiceImpl implements BiomeService {
}

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 0x537b09:
case 0x596651:
case 0x2d8e49:
case 0x628b17:
case Biome.DESERT:
case Biome.GIANT_TREE_TAIGA:
return true;
default:
break


+ 4
- 4
src/backend/services/Projection.service.ts Целия файл

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

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

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 {
@@ -33,14 +33,14 @@ export class ProjectionServiceImpl implements ProjectionService {

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) {
return equiImage;
}

const { [projection]: currentProjectionData } = this.projections
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;
if (!invert) {
return null;


+ 90
- 7
src/backend/services/Tile.service.ts Целия файл

@@ -1,5 +1,7 @@
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 = {
nw: boolean,
@@ -20,6 +22,9 @@ export default interface TileService {
}

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

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

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 (!this.rng.bool({ likelihood: 30 })) {
return 0
}
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
}

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

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

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

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

return 0;
}

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;
}



+ 11
- 1
src/components/GenerateMapForm/index.tsx Целия файл

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

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

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


+ 20
- 14
src/pages/api/generate/index.ts Целия файл

@@ -1,15 +1,16 @@
import {NextApiHandler} from 'next';
import * as fs from 'fs/promises';
import {PNG} from 'pngjs';
import {MapType, Projection, WorldData} from '../../../utils/types';
import {Bounds, MapType, Projection, WorldData} from '../../../utils/types';
import {Stats} from 'fs';
import sharp from 'sharp';
import ProjectionService, {ProjectionServiceImpl} from '../../../backend/services/Projection.service';
import {FileSystemServiceImpl} from '../../../backend/services/FileSystem.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 shouldGenerateFile = false;
try {
@@ -27,7 +28,7 @@ const generateProjectedBaseData = async (t: MapType, p: Projection) => {
const inputBuffer = await fs.readFile(`src/assets/data/000/${t}.png`)
const inputPng = PNG.sync.read(inputBuffer);
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);
await fs.writeFile(destPath, outputBuffer);
}
@@ -39,6 +40,7 @@ const getResizeKernel = (t: MapType) => {
switch (t) {
case MapType.BIOME:
case MapType.BATHYMETRY:
case MapType.CLOUDS:
return 'nearest';
case MapType.WATER_MASK:
return 'lanczos2';
@@ -48,9 +50,9 @@ const getResizeKernel = (t: MapType) => {
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({
[isWidthDimension ? 'width' : 'height']: size,
kernel: getResizeKernel(t),
@@ -68,18 +70,18 @@ const resizeBaseData = async (t: MapType, p: Projection, size: number, isWidthDi
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');
const tempResizedFiles = await Promise.all(
Object
.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) => {
return {
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.ELEVATION].data,
data[MapType.WATER_MASK].data,
data[MapType.BATHYMETRY].data
data[MapType.BATHYMETRY].data,
data[MapType.CLOUDS].data,
)

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


+ 9
- 23
src/pages/index.tsx Целия файл

@@ -18,22 +18,6 @@ import WORLD_FOREGROUND from '../assets/gfx/world_foreground.png';
import {DropdownSelect} from '../components/DropdownSelect';
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) => {
return new Promise<HTMLImageElement>((resolve, reject) => {
const originalImage = new Image()
@@ -235,8 +219,9 @@ ${new Array(worldData.height).fill(new Array(worldData.width).fill('0').join(','

const IndexPage: NextPage = () => {
const [data, setData] = useState<Data>()
const [loading, setLoading] = useState(false)
const [previewMapOpacity, setPreviewMapOpacity] = useState<Record<MapType, number>>(() => {
return IMAGE_URLS_INDEX.reduce(
return Object.values(MapType).reduce(
(theValue, mapType) => {
return {
...theValue,
@@ -252,11 +237,11 @@ const IndexPage: NextPage = () => {
const scrollRef = useRef(false)
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
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 search = new URLSearchParams(values)
url.search = search.toString()
setLoading(true)
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
@@ -267,6 +252,7 @@ const IndexPage: NextPage = () => {
if (response.ok) {
setData(responseData)
}
setLoading(false)
}

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


+ 47
- 2
src/utils/types.ts Целия файл

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

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

@@ -45,3 +46,47 @@ export enum LandType {
BEACH = 5,
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 Целия файл

@@ -176,6 +176,11 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
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@*":
version "3.0.2"
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"
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:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"


Зареждане…
Отказ
Запис