diff --git a/src/app/Main.tsx b/src/app/Main.tsx index d1478fee..e2286d73 100644 --- a/src/app/Main.tsx +++ b/src/app/Main.tsx @@ -2,20 +2,24 @@ import { render } from 'preact' import '../styles/global.css' import '../styles/nodes.css' import { App } from './App' -import { LocaleProvider, ProjectProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts' +import { LocaleProvider, ProjectProvider, StoreProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts' function Main() { - return - - - - - - - - - - + return ( + + + + + + + + + + + + + + ) } render(
, document.body) diff --git a/src/app/Utils.ts b/src/app/Utils.ts index dd8e9759..35d893e4 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -13,12 +13,14 @@ export function isObject(obj: any): obj is Record { 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) { var arr = new Uint8Array(length / 2) window.crypto.getRandomValues(arr) - return Array.from(arr, dec2hex).join('') + return Array.from(arr, decToHex).join('') } export function randomSeed() { @@ -152,11 +154,28 @@ function findMatchingClose(source: string, index: number) { 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)) 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) { return a * a } diff --git a/src/app/components/previews/BiomeSourcePreview.tsx b/src/app/components/previews/BiomeSourcePreview.tsx index be5b7e3d..cb5de93b 100644 --- a/src/app/components/previews/BiomeSourcePreview.tsx +++ b/src/app/components/previews/BiomeSourcePreview.tsx @@ -3,7 +3,7 @@ import type { NoiseParameters } from 'deepslate/worldgen' import { useEffect, useMemo, useRef, useState } from 'preact/hooks' import type { PreviewProps } from '.' import { Btn, BtnMenu } from '..' -import { useLocale } from '../../contexts' +import { useLocale, useStore } from '../../contexts' import { useCanvas } from '../../hooks' import { biomeMap, getBiome } from '../../previews' import { newSeed, randomSeed } from '../../Utils' @@ -16,6 +16,7 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps const [scale, setScale] = useState(2) const [focused, setFocused] = useState<{[k: string]: number | string} | undefined>(undefined) const [layers, setLayers] = useState(new Set(['biomes'])) + const { biomeColors } = useStore() const offset = useRef<[number, number]>([0, 0]) const res = useRef(1) const refineTimeout = useRef() @@ -33,7 +34,7 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps return [200 / res.current, 200 / res.current] }, 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) if (res.current === 4) { clearTimeout(refineTimeout.current) @@ -51,21 +52,21 @@ export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps redraw() }, 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) setFocused(biome) }, onLeave() { setFocused(undefined) }, - }, [version, state, scale, configuredSeed, layers]) + }, [version, state, scale, configuredSeed, layers, biomeColors]) useEffect(() => { if (shown) { res.current = type === 'multi_noise' ? 4 : 1 redraw() } - }, [version, state, scale, configuredSeed, layers, shown]) + }, [version, state, scale, configuredSeed, layers, shown, biomeColors]) const changeScale = (newScale: number) => { offset.current[0] = offset.current[0] * scale / newScale diff --git a/src/app/contexts/Store.tsx b/src/app/contexts/Store.tsx new file mode 100644 index 00000000..a737f1f5 --- /dev/null +++ b/src/app/contexts/Store.tsx @@ -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 + setBiomeColor: (biome: string, color: Color) => void +} + +const Store = createContext({ + biomeColors: {}, + setBiomeColor: () => {}, +}) + +export function useStore() { + return useContext(Store) +} + +export function StoreProvider({ children }: { children: ComponentChildren }) { + const [biomeColors, setBiomeColors] = useLocalStorage>('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 + {children} + +} diff --git a/src/app/contexts/index.ts b/src/app/contexts/index.ts index 4d220acd..baba5b78 100644 --- a/src/app/contexts/index.ts +++ b/src/app/contexts/index.ts @@ -1,5 +1,6 @@ export * from './Locale' export * from './Project' +export * from './Store' export * from './Theme' export * from './Title' export * from './Version' diff --git a/src/app/hooks/index.ts b/src/app/hooks/index.ts index ff65a4b7..70c8467c 100644 --- a/src/app/hooks/index.ts +++ b/src/app/hooks/index.ts @@ -4,6 +4,7 @@ export * from './useAsyncFn' export * from './useCanvas' export * from './useFocus' export * from './useHash' +export * from './useLocalStorage' export * from './useMediaQuery' export * from './useModel' export * from './useSearchParam' diff --git a/src/app/hooks/useLocalStorage.ts b/src/app/hooks/useLocalStorage.ts new file mode 100644 index 00000000..de60b5d1 --- /dev/null +++ b/src/app/hooks/useLocalStorage.ts @@ -0,0 +1,35 @@ +import { useCallback, useState } from 'preact/hooks' + +type Result = [T, (value: T | null | undefined) => void] + +export function useLocalStorage(key: string, defaultValue: T): Result +export function useLocalStorage(key: string, defaultValue: T, parse: (s: string) => T, stringify: (e: T) => string): Result +export function useLocalStorage(key: string, defaultValue: T, parse?: (s: string) => T, stringify?: (e: T) => string): Result { + 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] +} diff --git a/src/app/previews/BiomeSource.ts b/src/app/previews/BiomeSource.ts index 0e12fce9..0da574af 100644 --- a/src/app/previews/BiomeSource.ts +++ b/src/app/previews/BiomeSource.ts @@ -246,7 +246,7 @@ function toWorld([x, z]: [number, number], options: BiomeSourceOptions) { return [xx, zz] } -const VanillaColors: Record = { +export const VanillaColors: Record = { 'minecraft:badlands': [217,69,21], 'minecraft:badlands_plateau': [202,140,101], 'minecraft:bamboo_jungle': [118,142,20], diff --git a/src/app/schema/renderHtml.tsx b/src/app/schema/renderHtml.tsx index 24e498dd..a6a98c6e 100644 --- a/src/app/schema/renderHtml.tsx +++ b/src/app/schema/renderHtml.tsx @@ -5,11 +5,12 @@ import { memo } from 'preact/compat' import { useState } from 'preact/hooks' import config from '../../config.json' import { Btn, Octicon } from '../components' -import { localize } from '../contexts' +import { localize, useStore } from '../contexts' import { useFocus } from '../hooks' +import { VanillaColors } from '../previews' import type { BlockStateRegistry, VersionId } 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' 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 => )} } else { + const { biomeColors, setBiomeColor } = useStore() + const fullId = typeof value === 'string' ? value.includes(':') ? value : 'minecraft:' + value : 'unknown' const datalistId = hexId() const gen = id ? findGenerator(id) : undefined return <> @@ -424,7 +427,8 @@ function StringSuffix({ path, getValues, config, node, value, lang, version, sta {values.length > 0 && {values.map(v => } - {['attribute_modifier.id', 'text_component_object.hoverEvent.show_entity.contents.id'].includes(path.getContext().join('.')) && } + {['generator_biome.biome'].includes(context) && setBiomeColor(fullId, hexToRgb(v.currentTarget.value))}>} + {['attribute_modifier.id', 'text_component_object.hoverEvent.show_entity.contents.id'].includes(context) && } {gen && values.includes(value) && value.startsWith('minecraft:') && {Octicon.link_external}}