Configurable biome colors in preview

- add Store context
- add useLocalStorage
- color utils
This commit is contained in:
Misode
2022-05-14 19:39:48 +02:00
parent 114164c740
commit 2ebba08d4b
9 changed files with 125 additions and 24 deletions
+16 -12
View File
@@ -2,20 +2,24 @@ import { render } from 'preact'
import '../styles/global.css' import '../styles/global.css'
import '../styles/nodes.css' import '../styles/nodes.css'
import { App } from './App' import { App } from './App'
import { LocaleProvider, ProjectProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts' import { LocaleProvider, ProjectProvider, StoreProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts'
function Main() { function Main() {
return <LocaleProvider> return (
<ThemeProvider> <StoreProvider>
<VersionProvider> <LocaleProvider>
<TitleProvider> <ThemeProvider>
<ProjectProvider> <VersionProvider>
<App /> <TitleProvider>
</ProjectProvider> <ProjectProvider>
</TitleProvider> <App />
</VersionProvider> </ProjectProvider>
</ThemeProvider> </TitleProvider>
</LocaleProvider> </VersionProvider>
</ThemeProvider>
</LocaleProvider>
</StoreProvider>
)
} }
render(<Main />, document.body) render(<Main />, document.body)
+22 -3
View File
@@ -13,12 +13,14 @@ export function isObject(obj: any): obj is Record<string, any> {
return typeof obj === 'object' && obj !== null return typeof obj === 'object' && obj !== null
} }
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2) function decToHex(n: number) {
return n.toString(16).padStart(2, '0')
}
export function hexId(length = 12) { export function hexId(length = 12) {
var arr = new Uint8Array(length / 2) var arr = new Uint8Array(length / 2)
window.crypto.getRandomValues(arr) window.crypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('') return Array.from(arr, decToHex).join('')
} }
export function randomSeed() { export function randomSeed() {
@@ -152,11 +154,28 @@ function findMatchingClose(source: string, index: number) {
return source.length return source.length
} }
export function stringToColor(str: string): [number, number, number] { export type Color = [number, number, number]
export function stringToColor(str: string): Color {
const h = Math.abs(hashString(str)) const h = Math.abs(hashString(str))
return [h % 256, (h >> 8) % 256, (h >> 16) % 256] return [h % 256, (h >> 8) % 256, (h >> 16) % 256]
} }
export function rgbToHex(color: Color): string {
if (!Array.isArray(color) || color.length !== 3) return '#000000'
const [r, g, b] = color
return '#' + decToHex(r) + decToHex(g) + decToHex(b)
}
export function hexToRgb(hex: string | undefined): Color {
if (typeof hex !== 'string') return [0, 0, 0]
const num = parseInt(hex.startsWith('#') ? hex.slice(1) : hex, 16)
const r = (num >> 16) & 255
const g = (num >> 8) & 255
const b = num & 255
return [r, g, b]
}
export function square(a: number) { export function square(a: number) {
return a * a return a * a
} }
@@ -3,7 +3,7 @@ import type { NoiseParameters } from 'deepslate/worldgen'
import { useEffect, useMemo, useRef, useState } from 'preact/hooks' import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import type { PreviewProps } from '.' import type { PreviewProps } from '.'
import { Btn, BtnMenu } from '..' import { Btn, BtnMenu } from '..'
import { useLocale } from '../../contexts' import { useLocale, useStore } from '../../contexts'
import { useCanvas } from '../../hooks' import { useCanvas } from '../../hooks'
import { biomeMap, getBiome } from '../../previews' import { biomeMap, getBiome } from '../../previews'
import { newSeed, randomSeed } from '../../Utils' import { newSeed, randomSeed } from '../../Utils'
@@ -16,6 +16,7 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps
const [scale, setScale] = useState(2) const [scale, setScale] = useState(2)
const [focused, setFocused] = useState<{[k: string]: number | string} | undefined>(undefined) const [focused, setFocused] = useState<{[k: string]: number | string} | undefined>(undefined)
const [layers, setLayers] = useState(new Set<typeof LAYERS[number]>(['biomes'])) const [layers, setLayers] = useState(new Set<typeof LAYERS[number]>(['biomes']))
const { biomeColors } = useStore()
const offset = useRef<[number, number]>([0, 0]) const offset = useRef<[number, number]>([0, 0])
const res = useRef(1) const res = useRef(1)
const refineTimeout = useRef<number>() const refineTimeout = useRef<number>()
@@ -33,7 +34,7 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps
return [200 / res.current, 200 / res.current] return [200 / res.current, 200 / res.current]
}, },
async draw(img) { async draw(img) {
const options = { octaves: octaves!, biomeColors: {}, layers, offset: offset.current, scale, seed, res: res.current, version } const options = { octaves: octaves!, biomeColors, layers, offset: offset.current, scale, seed, res: res.current, version }
await biomeMap(data, img, options) await biomeMap(data, img, options)
if (res.current === 4) { if (res.current === 4) {
clearTimeout(refineTimeout.current) clearTimeout(refineTimeout.current)
@@ -51,21 +52,21 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps
redraw() redraw()
}, },
async onHover(x, y) { async onHover(x, y) {
const options = { octaves: octaves!, biomeColors: {}, layers, offset: offset.current, scale, seed: configuredSeed, res: 1, version } const options = { octaves: octaves!, biomeColors, layers, offset: offset.current, scale, seed: configuredSeed, res: 1, version }
const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options) const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options)
setFocused(biome) setFocused(biome)
}, },
onLeave() { onLeave() {
setFocused(undefined) setFocused(undefined)
}, },
}, [version, state, scale, configuredSeed, layers]) }, [version, state, scale, configuredSeed, layers, biomeColors])
useEffect(() => { useEffect(() => {
if (shown) { if (shown) {
res.current = type === 'multi_noise' ? 4 : 1 res.current = type === 'multi_noise' ? 4 : 1
redraw() redraw()
} }
}, [version, state, scale, configuredSeed, layers, shown]) }, [version, state, scale, configuredSeed, layers, shown, biomeColors])
const changeScale = (newScale: number) => { const changeScale = (newScale: number) => {
offset.current[0] = offset.current[0] * scale / newScale offset.current[0] = offset.current[0] * scale / newScale
+36
View File
@@ -0,0 +1,36 @@
import type { ComponentChildren } from 'preact'
import { createContext } from 'preact'
import { useCallback, useContext } from 'preact/hooks'
import { useLocalStorage } from '../hooks'
import type { Color } from '../Utils'
interface Store {
biomeColors: Record<string, [number, number, number]>
setBiomeColor: (biome: string, color: Color) => void
}
const Store = createContext<Store>({
biomeColors: {},
setBiomeColor: () => {},
})
export function useStore() {
return useContext(Store)
}
export function StoreProvider({ children }: { children: ComponentChildren }) {
const [biomeColors, setBiomeColors] = useLocalStorage<Record<string, Color>>('misode_biome_colors', {}, JSON.parse, JSON.stringify)
const setBiomeColor = useCallback((biome: string, color: Color) => {
setBiomeColors({...biomeColors, [biome]: color })
}, [biomeColors])
const value: Store = {
biomeColors,
setBiomeColor,
}
return <Store.Provider value={value}>
{children}
</Store.Provider>
}
+1
View File
@@ -1,5 +1,6 @@
export * from './Locale' export * from './Locale'
export * from './Project' export * from './Project'
export * from './Store'
export * from './Theme' export * from './Theme'
export * from './Title' export * from './Title'
export * from './Version' export * from './Version'
+1
View File
@@ -4,6 +4,7 @@ export * from './useAsyncFn'
export * from './useCanvas' export * from './useCanvas'
export * from './useFocus' export * from './useFocus'
export * from './useHash' export * from './useHash'
export * from './useLocalStorage'
export * from './useMediaQuery' export * from './useMediaQuery'
export * from './useModel' export * from './useModel'
export * from './useSearchParam' export * from './useSearchParam'
+35
View File
@@ -0,0 +1,35 @@
import { useCallback, useState } from 'preact/hooks'
type Result<T> = [T, (value: T | null | undefined) => void]
export function useLocalStorage<T = string>(key: string, defaultValue: T): Result<T>
export function useLocalStorage<T>(key: string, defaultValue: T, parse: (s: string) => T, stringify: (e: T) => string): Result<T>
export function useLocalStorage<T>(key: string, defaultValue: T, parse?: (s: string) => T, stringify?: (e: T) => string): Result<T> {
const getter = useCallback(() => {
const raw = localStorage.getItem(key)
if (raw === null) {
return defaultValue
} else if (parse === undefined) {
return raw as unknown as T
} else {
return parse(raw)
}
}, [])
const [state, setState] = useState(getter())
const setter = useCallback((value: T | null | undefined) => {
if (value == null) {
localStorage.removeItem(key)
setState(defaultValue)
} else if (stringify !== undefined) {
localStorage.setItem(key, stringify(value))
setState(value)
} else {
localStorage.setItem(key, value as unknown as string)
setState(value)
}
}, [])
return [state, setter]
}
+1 -1
View File
@@ -246,7 +246,7 @@ function toWorld([x, z]: [number, number], options: BiomeSourceOptions) {
return [xx, zz] return [xx, zz]
} }
const VanillaColors: Record<string, Triple> = { export const VanillaColors: Record<string, Triple> = {
'minecraft:badlands': [217,69,21], 'minecraft:badlands': [217,69,21],
'minecraft:badlands_plateau': [202,140,101], 'minecraft:badlands_plateau': [202,140,101],
'minecraft:bamboo_jungle': [118,142,20], 'minecraft:bamboo_jungle': [118,142,20],
+7 -3
View File
@@ -5,11 +5,12 @@ import { memo } from 'preact/compat'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
import config from '../../config.json' import config from '../../config.json'
import { Btn, Octicon } from '../components' import { Btn, Octicon } from '../components'
import { localize } from '../contexts' import { localize, useStore } from '../contexts'
import { useFocus } from '../hooks' import { useFocus } from '../hooks'
import { VanillaColors } from '../previews'
import type { BlockStateRegistry, VersionId } from '../services' import type { BlockStateRegistry, VersionId } from '../services'
import { CachedDecorator, CachedFeature } from '../services' import { CachedDecorator, CachedFeature } from '../services'
import { deepClone, deepEqual, generateUUID, hexId, isObject, newSeed } from '../Utils' import { deepClone, deepEqual, generateUUID, hexId, hexToRgb, isObject, newSeed, rgbToHex, stringToColor } from '../Utils'
import { ModelWrapper } from './ModelWrapper' import { ModelWrapper } from './ModelWrapper'
const selectRegistries = ['loot_table.type', 'loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'recipe.type', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'dimension.generator.biome_source.preset', 'carver.type', 'feature.type', 'decorator.type', 'feature.tree.minimum_size.type', 'block_state_provider.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'structure_feature.type', 'surface_builder.type', 'processor.processor_type', 'rule_test.predicate_type', 'pos_rule_test.predicate_type', 'template_element.element_type', 'block_placer.type', 'block_predicate.type', 'material_rule.type', 'material_condition.type', 'structure_placement.type', 'density_function.type', 'root_placer.type', 'entity.type_specific.cat.variant', 'entity.type_specific.frog.variant'] const selectRegistries = ['loot_table.type', 'loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'recipe.type', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'dimension.generator.biome_source.preset', 'carver.type', 'feature.type', 'decorator.type', 'feature.tree.minimum_size.type', 'block_state_provider.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'structure_feature.type', 'surface_builder.type', 'processor.processor_type', 'rule_test.predicate_type', 'pos_rule_test.predicate_type', 'template_element.element_type', 'block_placer.type', 'block_predicate.type', 'material_rule.type', 'material_condition.type', 'structure_placement.type', 'density_function.type', 'root_placer.type', 'entity.type_specific.cat.variant', 'entity.type_specific.frog.variant']
@@ -416,6 +417,8 @@ function StringSuffix({ path, getValues, config, node, value, lang, version, sta
{values.map(v => <option>{v}</option>)} {values.map(v => <option>{v}</option>)}
</select> </select>
} else { } else {
const { biomeColors, setBiomeColor } = useStore()
const fullId = typeof value === 'string' ? value.includes(':') ? value : 'minecraft:' + value : 'unknown'
const datalistId = hexId() const datalistId = hexId()
const gen = id ? findGenerator(id) : undefined const gen = id ? findGenerator(id) : undefined
return <> return <>
@@ -424,7 +427,8 @@ function StringSuffix({ path, getValues, config, node, value, lang, version, sta
{values.length > 0 && <datalist id={datalistId}> {values.length > 0 && <datalist id={datalistId}>
{values.map(v => <option value={v} />)} {values.map(v => <option value={v} />)}
</datalist>} </datalist>}
{['attribute_modifier.id', 'text_component_object.hoverEvent.show_entity.contents.id'].includes(path.getContext().join('.')) && <button onClick={() => path.set(generateUUID())} class="tooltipped tip-se" aria-label={localize(lang, 'generate_new_uuid')}>{Octicon.sync}</button>} {['generator_biome.biome'].includes(context) && <input type="color" value={rgbToHex(biomeColors[fullId] ?? VanillaColors[fullId] ?? stringToColor(fullId))} onChange={v => setBiomeColor(fullId, hexToRgb(v.currentTarget.value))}></input>}
{['attribute_modifier.id', 'text_component_object.hoverEvent.show_entity.contents.id'].includes(context) && <button onClick={() => path.set(generateUUID())} class="tooltipped tip-se" aria-label={localize(lang, 'generate_new_uuid')}>{Octicon.sync}</button>}
{gen && values.includes(value) && value.startsWith('minecraft:') && {gen && values.includes(value) && value.startsWith('minecraft:') &&
<a href={`/${gen.url}/?version=${version}&preset=${value.replace(/^minecraft:/, '')}`} class="tooltipped tip-se" aria-label={localize(lang, 'follow_reference')}>{Octicon.link_external}</a>} <a href={`/${gen.url}/?version=${version}&preset=${value.replace(/^minecraft:/, '')}`} class="tooltipped tip-se" aria-label={localize(lang, 'follow_reference')}>{Octicon.link_external}</a>}
</> </>