Add sounds explorer tool

This commit is contained in:
Misode
2021-10-08 02:34:38 +02:00
parent c1de35b6c2
commit 79b3291d06
23 changed files with 690 additions and 56 deletions
+2
View File
@@ -1,4 +1,5 @@
export const Octicon = {
alert: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>,
archive: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 2.5a.25.25 0 00-.25.25v1.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-1.5a.25.25 0 00-.25-.25H1.75zM0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v1.5A1.75 1.75 0 0114.25 6H1.75A1.75 1.75 0 010 4.25v-1.5zM1.75 7a.75.75 0 01.75.75v5.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25v-5.5a.75.75 0 111.5 0v5.5A1.75 1.75 0 0113.25 15H2.75A1.75 1.75 0 011 13.25v-5.5A.75.75 0 011.75 7zm4.5 1a.75.75 0 000 1.5h3.5a.75.75 0 100-1.5h-3.5z"></path></svg>,
arrow_left: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>,
arrow_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"></path></svg>,
@@ -30,6 +31,7 @@ export const Octicon = {
sun: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"></path></svg>,
sync: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"></path></svg>,
tag: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>,
terminal: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 11-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path></svg>,
three_bars: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>,
trashcan: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>,
unfold: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8.177.677l2.896 2.896a.25.25 0 01-.177.427H8.75v1.25a.75.75 0 01-1.5 0V4H5.104a.25.25 0 01-.177-.427L7.823.677a.25.25 0 01.354 0zM7.25 10.75a.75.75 0 011.5 0V12h2.146a.25.25 0 01.177.427l-2.896 2.896a.25.25 0 01-.354 0l-2.896-2.896A.25.25 0 015.104 12H7.25v-1.25zm-5-2a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5zM6 8a.75.75 0 01-.75.75h-.5a.75.75 0 010-1.5h.5A.75.75 0 016 8zm2.25.75a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5zM12 8a.75.75 0 01-.75.75h-.5a.75.75 0 010-1.5h.5A.75.75 0 0112 8zm2.25.75a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5z"></path></svg>,
+31
View File
@@ -0,0 +1,31 @@
import type { JSXInternal } from 'preact/src/jsx'
type InputProps = JSXInternal.HTMLAttributes<HTMLInputElement>
type BaseInputProps<T> = Omit<InputProps, 'onChange' | 'type'> & {
onChange?: (value: T) => unknown,
onEnter?: (value: T) => unknown,
}
function BaseInput<T>(name: string, type: string, fn: (value: string) => T) {
const component = (props: BaseInputProps<T>) => {
const onChange = props.onChange && ((evt: Event) => {
const value = (evt.target as HTMLInputElement).value
props.onChange?.(fn(value))
})
const onKeyDown = props.onEnter && ((evt: KeyboardEvent) => {
if (evt.key === 'Enter') {
const value = (evt.target as HTMLInputElement).value
props.onEnter?.(fn(value))
}
})
return <input {...props} {...{ type, onChange, onKeyDown }} />
}
component.displayName = name
return component
}
export const TextInput = BaseInput('TextInput', 'text', v => v)
export const NumberInput = BaseInput('NumberInput', 'number', v => Number(v))
export const RangeInput = BaseInput('RangeInput', 'range', v => Number(v))
+1
View File
@@ -0,0 +1 @@
export * from './Input'
@@ -1,9 +1,9 @@
import type { DataModel } from '@mcschema/core'
import { Path } from '@mcschema/core'
import { useState } from 'preact/hooks'
import { useModel } from '../hooks'
import type { VersionId } from '../Schemas'
import { BiomeSourcePreview, DecoratorPreview, NoiseSettingsPreview } from './previews'
import { useModel } from '../../hooks'
import type { VersionId } from '../../Schemas'
import { BiomeSourcePreview, DecoratorPreview, NoiseSettingsPreview } from '../previews'
export const HasPreview = ['dimension', 'worldgen/noise_settings', 'worldgen/configured_feature']
@@ -1,12 +1,12 @@
import { DataModel, ModelPath } from '@mcschema/core'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { Btn, BtnMenu } from '.'
import { useModel } from '../hooks'
import { locale } from '../Locales'
import { transformOutput } from '../schema/transformOutput'
import type { BlockStateRegistry } from '../Schemas'
import { Store } from '../Store'
import { message } from '../Utils'
import { Btn, BtnMenu } from '..'
import { useModel } from '../../hooks'
import { locale } from '../../Locales'
import { transformOutput } from '../../schema/transformOutput'
import type { BlockStateRegistry } from '../../Schemas'
import { Store } from '../../Store'
import { message } from '../../Utils'
const OUTPUT_CHARS_LIMIT = 10000
@@ -1,8 +1,8 @@
import type { DataModel } from '@mcschema/core'
import { useErrorBoundary, useState } from 'preact/hooks'
import { useModel } from '../hooks'
import { FullNode } from '../schema/renderHtml'
import type { BlockStateRegistry, VersionId } from '../Schemas'
import { useModel } from '../../hooks'
import { FullNode } from '../../schema/renderHtml'
import type { BlockStateRegistry, VersionId } from '../../Schemas'
type TreePanelProps = {
lang: string,
+3
View File
@@ -0,0 +1,3 @@
export * from './PreviewPanel'
export * from './SourcePanel'
export * from './Tree'
+4 -3
View File
@@ -3,10 +3,11 @@ export * from './Btn'
export * from './BtnInput'
export * from './BtnMenu'
export * from './ErrorPanel'
export * from './forms'
export * from './generator'
export * from './Header'
export * from './Icons'
export * from './Octicon'
export * from './PreviewPanel'
export * from './SourcePanel'
export * from './previews'
export * from './sounds'
export * from './ToolCard'
export * from './Tree'
+122
View File
@@ -0,0 +1,122 @@
import { Howl } from 'howler'
import { useEffect, useRef, useState } from 'preact/hooks'
import { Btn, NumberInput, RangeInput, TextInput } from '..'
import { getResourceUrl } from '../../DataFetcher'
import { locale } from '../../Locales'
import type { SoundEvents, VersionAssets } from '../../Manifest'
export interface SoundConfig {
id: string,
sound: string,
delay: number,
pitch: number,
volume: number,
}
type SoundConfigProps = SoundConfig & {
lang: string,
assets: VersionAssets,
sounds: SoundEvents,
onEdit: (changes: Partial<SoundConfig>) => unknown,
onDelete: () => unknown,
delayedPlay?: number,
}
export function SoundConfig({ lang, assets, sounds, sound, delay, pitch, volume, onEdit, onDelete, delayedPlay }: SoundConfigProps) {
const loc = locale.bind(null, lang)
const [loading, setLoading] = useState(true)
const [playing, setPlaying] = useState(false)
const [invalid, setInvalid] = useState(false)
const howls = useRef<Howl[]>([])
const command = `playsound minecraft:${sound} master @s ~ ~ ~ ${volume} ${pitch}`
useEffect(() => {
const soundEvent = sounds[sound]
setInvalid((soundEvent?.sounds?.length ?? 0) === 0)
howls.current.forEach(h => h.stop())
howls.current = (soundEvent?.sounds ?? []).map(entry => {
const soundPath = typeof entry === 'string' ? entry : entry.name
const hash = assets[`minecraft/sounds/${soundPath}.ogg`].hash
const url = getResourceUrl(hash)
const howl = new Howl({
src: [url],
format: ['ogg'],
volume,
rate: pitch,
})
howl.on('end', () => {
setPlaying(false)
})
const completed = () => {
if (loading && howls.current.every(h => h.state() === 'loaded')) {
setLoading(false)
}
}
if (howl.state() === 'loaded') {
setTimeout(() => completed())
} else {
howl.on('load', () => {
completed()
})
}
return howl
})
setLoading(true)
}, [sound, sounds])
useEffect(() => {
howls.current.forEach(h => h.rate(pitch))
}, [pitch])
useEffect(() => {
howls.current.forEach(h => h.volume(volume))
}, [volume])
const play = () => {
if (loading || invalid) return
stop()
const howl = Math.floor(Math.random() * howls.current.length)
howls.current[howl].play()
setPlaying(true)
}
const stop = () => {
howls.current.forEach(h => h.stop())
}
useEffect(() => {
if (delayedPlay) setTimeout(() => play(), delay * 50)
}, [delayedPlay])
useEffect(() => {
return () => stop()
}, [])
const [copyActive, setCopyActive] = useState(false)
const copyTimeout = useRef<number | undefined>(undefined)
const copy = () => {
navigator.clipboard.writeText(command)
setCopyActive(true)
if (copyTimeout.current !== undefined) clearTimeout(copyTimeout.current)
copyTimeout.current = setTimeout(() => {
setCopyActive(false)
}, 2000) as any
}
return <div class={`sound-config${loading ? ' loading' : playing ? ' playing' : ''}${invalid ? ' invalid' : ''}`}>
<Btn class="play" icon={invalid ? 'alert' : loading ? 'sync' : 'play'} label={loc('sounds.play')} onClick={play} tooltip={invalid ? loc('sounds.unknown_sound') : loading ? loc('sounds.loading_sound') : loc('sounds.play_sound')} tooltipLoc="se" />
<TextInput class="btn btn-input sound" list="sound-list" spellcheck={false}
value={sound} onChange={sound => onEdit({ sound })} />
<label class="delay-label">{loc('sounds.delay')}: </label>
<NumberInput class="btn btn-input delay" min={0}
value={delay} onChange={delay => onEdit({ delay })} />
<label class="pitch-label">{loc('sounds.pitch')}: </label>
<RangeInput class="pitch tooltipped tip-s" min={0.5} max={2} step={0.01}
aria-label={pitch.toFixed(2)} style={`--x: ${(pitch - 0.5) * (100 / 1.5)}%`}
value={pitch} onChange={pitch => onEdit({ pitch })} />
<label class="volume-label">{loc('sounds.volume')}: </label>
<RangeInput class="volume tooltipped tip-s" min={0} max={1} step={0.01}
aria-label={volume.toFixed(2)} style={`--x: ${volume * 100}%`}
value={volume} onChange={volume => onEdit({ volume })} />
<Btn class={`copy${copyActive ? ' active' : ''}`} icon={copyActive ? 'check' : 'terminal'} label={loc('copy')} tooltip={copyActive ? loc('copied') : loc('sounds.copy_command')}
onClick={copy} />
<Btn class="remove" icon="trashcan" tooltip={loc('sounds.remove_sound')}
onClick={() => {onDelete(); stop()}} />
</div>
}
+1
View File
@@ -0,0 +1 @@
export * from './SoundConfig'