Files
misode.github.io/src/app/components/previews/BiomeSourcePreview.tsx
2024-11-27 06:01:05 +01:00

276 lines
11 KiB
TypeScript

import { clampedMap } from 'deepslate'
import { mat3 } from 'gl-matrix'
import { useCallback, useRef, useState } from 'preact/hooks'
import { getWorldgenProjectData, useLocale, useProject, useStore, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/index.js'
import { checkVersion } from '../../services/Versions.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed, safeJsonParse, stringToColor } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { ColormapSelector } from './ColormapSelector.jsx'
import { DEEPSLATE } from './Deepslate.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
const LAYERS = ['biomes', 'temperature', 'vegetation', 'continents', 'erosion', 'ridges', 'depth'] as const
type Layer = typeof LAYERS[number]
const DETAIL_DELAY = 300
const DETAIL_SCALE = 2
export const BiomeSourcePreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const { project } = useProject()
const { biomeColors } = useStore()
const [seed, setSeed] = useState(randomSeed())
const [layer, setLayer] = useState<Layer>('biomes')
const [yOffset, setYOffset] = useState(64)
const [focused, setFocused] = useState<string[]>([])
const [focused2, setFocused2] = useState<string[]>([])
const text = docAndNode.doc.getText()
const data = safeJsonParse(text) ?? {}
const type: string = data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? ''
const hasRandomness = type === 'multi_noise' || type === 'the_end'
const { value } = useAsync(async function loadBiomeSource() {
const projectData = await getWorldgenProjectData(project)
await DEEPSLATE.loadVersion(version, projectData)
await DEEPSLATE.loadChunkGenerator(data?.generator?.settings, data?.generator?.biome_source, seed)
return {
biomeSource: { loaded: true },
noiseRouter: checkVersion(version, '1.19') ? DEEPSLATE.getNoiseRouter() : undefined,
}
}, [text, seed, project, version])
const { biomeSource, noiseRouter } = value ?? {}
const actualLayer = noiseRouter ? layer : 'biomes'
const ctx = useRef<CanvasRenderingContext2D>()
const imageData = useRef<ImageData>()
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
const detailCanvas = useRef<HTMLCanvasElement>(null)
const detailCtx = useRef<CanvasRenderingContext2D>()
const detailImageData = useRef<ImageData>()
const detailTimeout = useRef<number>()
const onSetup = useCallback(function onSetup(canvas: HTMLCanvasElement) {
ctx.current = canvas.getContext('2d') ?? undefined
detailCtx.current = detailCanvas.current?.getContext('2d') ?? undefined
}, [])
const onResize = useCallback(function onResize(width: number, height: number) {
if (ctx.current) {
imageData.current = ctx.current.getImageData(0, 0, width, height)
}
if (detailCtx.current && detailCanvas.current) {
detailCanvas.current.width = width * DETAIL_SCALE
detailCanvas.current.height = height * DETAIL_SCALE
detailImageData.current = detailCtx.current.getImageData(0, 0, width * DETAIL_SCALE, height * DETAIL_SCALE)
}
}, [])
const onDraw = useCallback(function onDraw(transform: mat3) {
if (!ctx.current || !imageData.current || !shown) return
function actualDraw(ctx: CanvasRenderingContext2D, img: ImageData, transform: mat3) {
if (actualLayer === 'biomes' && biomeSource) {
iterateWorld2D(img, transform, (x, y) => {
return DEEPSLATE.getBiome(x, yOffset, y)
}, (biome) => {
return getBiomeColor(biome, biomeColors)
})
} else if (actualLayer !== 'biomes' && noiseRouter) {
const df = noiseRouter[actualLayer]
const colorPicker = getColormap(colormap)
iterateWorld2D(img, transform, (x, y) => {
return df.compute({ x: x*4, y: yOffset, z: y*4 }) ?? 0
}, (density) => {
const color = colorPicker(clampedMap(density, -1, 1, 0, 1))
return [color[0] * 256, color[1] * 256, color[2] * 256]
})
}
ctx.putImageData(img, 0, 0)
}
actualDraw(ctx.current, imageData.current, transform)
detailCanvas.current?.classList.remove('visible')
clearTimeout(detailTimeout.current)
if (hasRandomness) {
detailTimeout.current = setTimeout(function detailTimout() {
if (!detailCtx.current || !detailImageData.current || !detailCanvas.current) return
const detailTransform = mat3.create()
mat3.scale(detailTransform, transform, [1/DETAIL_SCALE, 1/DETAIL_SCALE])
actualDraw(detailCtx.current, detailImageData.current, detailTransform)
detailCanvas.current.classList.add('visible')
}, DETAIL_DELAY) as unknown as number
}
}, [biomeSource, noiseRouter, actualLayer, colormap, shown, biomeColors, yOffset])
const onHover = useCallback(function onHover(pos: [number, number] | undefined) {
const [x, y] = pos ?? [0, 0]
if (!pos || !biomeSource) {
setFocused([])
} else {
const biome = DEEPSLATE.getBiome(x, yOffset, -y)
setFocused([biome.replace(/^minecraft:/, ''), `X=${x*4} Z=${-y*4}`])
}
if (!pos || !noiseRouter) {
setFocused2([])
} else {
setFocused2([LAYERS.flatMap(l => {
if (l === 'biomes') return []
const value = noiseRouter[l].compute({ x: x*4, y: yOffset, z: -y*4 })
return [`${locale(`layer.${l}`).charAt(0)}=${value.toPrecision(2)}`]
}).join(' ')])
}
}, [biomeSource, noiseRouter, yOffset])
return <>
{(hasRandomness && focused2) && <div class="controls secondary-controls">
{focused2.map(s => <Btn label={s} class="no-pointer" /> )}
</div>}
<div class="controls preview-controls">
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
{actualLayer !== 'biomes' && <ColormapSelector value={colormap} onChange={setColormap} />}
{hasRandomness && <>
<BtnMenu icon="stack" tooltip={locale('layer')}>
<div class="btn btn-input" onClick={e => e.stopPropagation()}>
<span>{locale('y')}</span>
<NumberInput value={yOffset} onChange={setYOffset} />
</div>
{checkVersion(version, '1.19') && LAYERS.map(l => <Btn label={locale(`layer.${l}`)} active={l === actualLayer} onClick={() => setLayer(l)} />)}
</BtnMenu>
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</>}
</div>
<div class="full-preview">
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={hasRandomness ? 8 : 2} />
{hasRandomness && <canvas class={'preview-details'} ref={detailCanvas} />}
</div>
</>
}
type Triple = [number, number, number]
type BiomeColors = Record<string, Triple>
function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple {
if (!biome) {
return [128, 128, 128]
}
const color = biomeColors[biome] ?? VanillaColors[biome]
if (color === undefined) {
return stringToColor(biome)
}
return color
}
export const VanillaColors: Record<string, Triple> = {
'minecraft:badlands': [217,69,21],
'minecraft:badlands_plateau': [202,140,101],
'minecraft:bamboo_jungle': [118,142,20],
'minecraft:bamboo_jungle_hills': [59,71,10],
'minecraft:basalt_deltas': [64,54,54],
'minecraft:beach': [250,222,85],
'minecraft:birch_forest': [48,116,68],
'minecraft:birch_forest_hills': [31,95,50],
'minecraft:cold_ocean': [32,32,112],
'minecraft:crimson_forest': [221,8,8],
'minecraft:dark_forest': [64,81,26],
'minecraft:dark_forest_hills': [104,121,66],
'minecraft:deep_cold_ocean': [32,32,56],
'minecraft:deep_frozen_ocean': [64,64,144],
'minecraft:deep_lukewarm_ocean': [0,0,64],
'minecraft:deep_ocean': [0,0,48],
'minecraft:deep_warm_ocean': [0,0,80],
'minecraft:desert': [250,148,24],
'minecraft:desert_hills': [210,95,18],
'minecraft:desert_lakes': [255,188,64],
'minecraft:end_barrens': [39,30,61],
'minecraft:end_highlands': [232,244,178],
'minecraft:end_midlands': [194,187,136],
'minecraft:eroded_badlands': [255,109,61],
'minecraft:flower_forest': [45,142,73],
'minecraft:forest': [5,102,33],
'minecraft:frozen_ocean': [112,112,214],
'minecraft:frozen_river': [160,160,255],
'minecraft:giant_spruce_taiga': [129,142,121],
'minecraft:old_growth_spruce_taiga': [129,142,121],
'minecraft:giant_spruce_taiga_hills': [109,119,102],
'minecraft:giant_tree_taiga': [89,102,81],
'minecraft:old_growth_pine_taiga': [89,102,81],
'minecraft:giant_tree_taiga_hills': [69,79,62],
'minecraft:gravelly_hills': [136,136,136],
'minecraft:gravelly_mountains': [136,136,136],
'minecraft:windswept_gravelly_hills': [136,136,136],
'minecraft:ice_spikes': [180,220,220],
'minecraft:jungle': [83,123,9],
'minecraft:jungle_edge': [98,139,23],
'minecraft:sparse_jungle': [98,139,23],
'minecraft:jungle_hills': [44,66,5],
'minecraft:lukewarm_ocean': [0,0,144],
'minecraft:modified_badlands_plateau': [242,180,141],
'minecraft:modified_gravelly_mountains': [120,152,120],
'minecraft:modified_jungle': [123,163,49],
'minecraft:modified_jungle_edge': [138,179,63],
'minecraft:modified_wooded_badlands_plateau': [216,191,141],
'minecraft:mountain_edge': [114,120,154],
'minecraft:extreme_hills': [96,96,96],
'minecraft:mountains': [96,96,96],
'minecraft:windswept_hills': [96,96,96],
'minecraft:mushroom_field_shore': [160,0,255],
'minecraft:mushroom_fields': [255,0,255],
'minecraft:nether_wastes': [191,59,59],
'minecraft:ocean': [0,0,112],
'minecraft:plains': [141,179,96],
'minecraft:river': [0,0,255],
'minecraft:savanna': [189,178,95],
'minecraft:savanna_plateau': [167,157,100],
'minecraft:shattered_savanna': [229,218,135],
'minecraft:windswept_savanna': [229,218,135],
'minecraft:shattered_savanna_plateau': [207,197,140],
'minecraft:small_end_islands': [16,12,28],
'minecraft:snowy_beach': [250,240,192],
'minecraft:snowy_mountains': [160,160,160],
'minecraft:snowy_taiga': [49,85,74],
'minecraft:snowy_taiga_hills': [36,63,54],
'minecraft:snowy_taiga_mountains': [89,125,114],
'minecraft:snowy_tundra': [255,255,255],
'minecraft:snowy_plains': [255,255,255],
'minecraft:soul_sand_valley': [94,56,48],
'minecraft:stone_shore': [162,162,132],
'minecraft:stony_shore': [162,162,132],
'minecraft:sunflower_plains': [181,219,136],
'minecraft:swamp': [7,249,178],
'minecraft:swamp_hills': [47,255,218],
'minecraft:taiga': [11,102,89],
'minecraft:taiga_hills': [22,57,51],
'minecraft:taiga_mountains': [51,142,129],
'minecraft:tall_birch_forest': [88,156,108],
'minecraft:old_growth_birch_forest': [88,156,108],
'minecraft:tall_birch_hills': [71,135,90],
'minecraft:the_end': [59,39,84],
'minecraft:the_void': [0,0,0],
'minecraft:warm_ocean': [0,0,172],
'minecraft:warped_forest': [73,144,123],
'minecraft:wooded_badlands_plateau': [176,151,101],
'minecraft:wooded_badlands': [176,151,101],
'minecraft:wooded_hills': [34,85,28],
'minecraft:wooded_mountains': [80,112,80],
'minecraft:windswept_forest': [80,112,80],
'minecraft:snowy_slopes': [140, 195, 222],
'minecraft:lofty_peaks': [196, 168, 193],
'minecraft:jagged_peaks': [196, 168, 193],
'minecraft:snowcapped_peaks': [200, 198, 200],
'minecraft:frozen_peaks': [200, 198, 200],
'minecraft:stony_peaks': [82, 92, 103],
'minecraft:grove': [150, 150, 189],
'minecraft:meadow': [169, 197, 80],
'minecraft:lush_caves': [112, 255, 79],
'minecraft:dripstone_caves': [140, 124, 0],
'minecraft:deep_dark': [10, 14, 19],
'minecraft:mangrove_swamp': [36,196,142],
}