瀏覽代碼

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 查看文件

Before After
Width: 4608  |  Height: 2304  |  Size: 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"


Loading…
取消
儲存