Add background music; add favicon; add sound module
This commit is contained in:
+73
-24
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import UIfx from 'uifx'
|
||||
import { KeyboardEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import Sound from '../modules/sound'
|
||||
|
||||
import { PanelComponent, PreviewComponent, SettingsComponent, ViewComponent } from '../components'
|
||||
import places from '../assets'
|
||||
import { delay, UI_SOUND_VOLUME } from '../utils'
|
||||
import { delay, UI_MUSIC_VOLUME, UI_SOUND_VOLUME } from '../utils'
|
||||
import { useSettings } from '../hooks'
|
||||
|
||||
import PanelOpenAudio from '../assets/audio/sound/panel-open.ogg'
|
||||
@@ -16,23 +16,37 @@ import SettingsCloseAudio from '../assets/audio/sound/menu-close.ogg'
|
||||
import CheckBoxOnAudio from '../assets/audio/sound/check-box-on.ogg'
|
||||
import CheckBoxOffAudio from '../assets/audio/sound/check-box-off.ogg'
|
||||
|
||||
import StormwindParkMusic1 from '../assets/audio/music/stormwind-park-music-1.mp3'
|
||||
import StormwindParkMusic2 from '../assets/audio/music/stormwind-park-music-2.mp3'
|
||||
|
||||
import './style.scss'
|
||||
|
||||
export default function App() {
|
||||
const {settings} = useSettings()
|
||||
const [isSettingsShown, setSettingsShown] = useState(false)
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [isPlaying, setPlaying] = useState(false)
|
||||
const [isLeftPanelShown, setLeftPanelShown] = useState(false)
|
||||
const [isBottomPanelShown, setBottomPanelShown] = useState(false)
|
||||
const [activePlace, setActivePlace] = useState(0)
|
||||
const [activeView, setActiveView] = useState(0)
|
||||
|
||||
const panelOpenSound = useMemo(() => new UIfx(PanelOpenAudio), [])
|
||||
const panelCloseSound = useMemo(() => new UIfx(PanelCloseAudio), [])
|
||||
const settingsOpenSound = useMemo(() => new UIfx(SettingsOpenAudio), [])
|
||||
const settingsCloseSound = useMemo(() => new UIfx(SettingsCloseAudio), [])
|
||||
const checkboxOnSound = useMemo(() => new UIfx(CheckBoxOnAudio), [])
|
||||
const checkboxOffSound = useMemo(() => new UIfx(CheckBoxOffAudio), [])
|
||||
const soundLoad = (soundFile: string, soundVolume: number) => {
|
||||
const sound = new Sound(soundFile)
|
||||
sound.setVolume(soundVolume)
|
||||
return sound
|
||||
}
|
||||
|
||||
const panelOpenSound = useMemo(() => soundLoad(PanelOpenAudio, UI_SOUND_VOLUME), [])
|
||||
const panelCloseSound = useMemo(() => soundLoad(PanelCloseAudio, UI_SOUND_VOLUME), [])
|
||||
const settingsOpenSound = useMemo(() => soundLoad(SettingsOpenAudio, UI_SOUND_VOLUME), [])
|
||||
const settingsCloseSound = useMemo(() => soundLoad(SettingsCloseAudio, UI_SOUND_VOLUME), [])
|
||||
const checkboxOnSound = useMemo(() => soundLoad(CheckBoxOnAudio, UI_SOUND_VOLUME), [])
|
||||
const checkboxOffSound = useMemo(() => soundLoad(CheckBoxOffAudio, UI_SOUND_VOLUME), [])
|
||||
|
||||
const StormwindMusic1 = useMemo(() => soundLoad(StormwindParkMusic1, UI_MUSIC_VOLUME), [])
|
||||
const StormwindMusic2 = useMemo(() => soundLoad(StormwindParkMusic2, UI_MUSIC_VOLUME), [])
|
||||
const [currentPlaying, setCurrentPlaying] = useState(StormwindMusic1)
|
||||
|
||||
const app = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -62,29 +76,56 @@ export default function App() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
appFocus()
|
||||
}, [])
|
||||
|
||||
const appFocus = () => {
|
||||
if (app && app.current) {
|
||||
app.current.focus()
|
||||
}
|
||||
}
|
||||
}, [app])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.title = settings.language['place.stormwind-park']
|
||||
}, [settings.language])
|
||||
|
||||
useEffect(() => {
|
||||
StormwindMusic1.audio.onplay = () => {
|
||||
setCurrentPlaying(StormwindMusic1)
|
||||
setPlaying(true)
|
||||
}
|
||||
StormwindMusic2.audio.onplay = () => {
|
||||
setCurrentPlaying(StormwindMusic2)
|
||||
setPlaying(true)
|
||||
}
|
||||
StormwindMusic1.audio.onended = () => {
|
||||
StormwindMusic2.playMusic()
|
||||
}
|
||||
StormwindMusic2.audio.onended = () => {
|
||||
StormwindMusic1.playMusic()
|
||||
}
|
||||
return () => {
|
||||
StormwindMusic1.audio.onplay = null
|
||||
StormwindMusic2.audio.onplay = null
|
||||
StormwindMusic1.audio.onended = null
|
||||
StormwindMusic2.audio.onended = null
|
||||
}
|
||||
}, [StormwindMusic1, StormwindMusic2])
|
||||
|
||||
const appClick = () => currentPlaying.playMusic()
|
||||
|
||||
const openCloseSettings = () => {
|
||||
setSettingsShown(!isSettingsShown)
|
||||
appFocus()
|
||||
if (app && app.current) {
|
||||
app.current.focus()
|
||||
}
|
||||
if (!settings.uiSound) {
|
||||
return
|
||||
}
|
||||
if (isSettingsShown) {
|
||||
settingsCloseSound.play(UI_SOUND_VOLUME)
|
||||
settingsCloseSound.playSound()
|
||||
} else {
|
||||
settingsOpenSound.play(UI_SOUND_VOLUME)
|
||||
settingsOpenSound.playSound()
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenSettings = (e: React.KeyboardEvent) => {
|
||||
const handleOpenSettings = (e: KeyboardEvent) => {
|
||||
switch (e.keyCode) {
|
||||
case 27:
|
||||
if (isLeftPanelShown || isBottomPanelShown) {
|
||||
@@ -94,20 +135,28 @@ export default function App() {
|
||||
}
|
||||
openCloseSettings()
|
||||
break
|
||||
case 32:
|
||||
if (isPlaying) {
|
||||
currentPlaying.pause()
|
||||
setPlaying(false)
|
||||
} else {
|
||||
currentPlaying.playMusic()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={app}
|
||||
onClick={appClick}
|
||||
onKeyDown={handleOpenSettings}
|
||||
tabIndex={0}
|
||||
className='main'
|
||||
>
|
||||
<ViewComponent src={places[activePlace].view[activeView]}/>
|
||||
<PanelComponent
|
||||
openSound={panelOpenSound}
|
||||
closeSound={panelCloseSound}
|
||||
openSoundPlay={panelOpenSound.playSound}
|
||||
closeSoundPlay={panelCloseSound.playSound}
|
||||
itemsCount={places.length || 0}
|
||||
orientation='left'
|
||||
isShown={isLeftPanelShown}
|
||||
@@ -124,8 +173,8 @@ export default function App() {
|
||||
))}
|
||||
</PanelComponent>
|
||||
<PanelComponent
|
||||
openSound={panelOpenSound}
|
||||
closeSound={panelCloseSound}
|
||||
openSoundPlay={panelOpenSound.playSound}
|
||||
closeSoundPlay={panelCloseSound.playSound}
|
||||
itemsCount={places[activePlace].preview.length || 0}
|
||||
orientation='bottom'
|
||||
isShown={isBottomPanelShown}
|
||||
@@ -144,8 +193,8 @@ export default function App() {
|
||||
{isSettingsShown && (
|
||||
<SettingsComponent
|
||||
closeSettings={openCloseSettings}
|
||||
checkboxOnSound={checkboxOnSound}
|
||||
checkboxOffSound={checkboxOffSound}
|
||||
checkboxOnSoundPlay={checkboxOnSound.playSound}
|
||||
checkboxOffSoundPlay={checkboxOffSound.playSound}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import { FocusEvent, MouseEvent, useEffect, useMemo, useRef, useState, WheelEvent } from 'react'
|
||||
import UIfx from 'uifx'
|
||||
|
||||
import { PREVIEW_HEIGHT, PREVIEW_WIDTH, UI_SOUND_VOLUME } from '../../utils'
|
||||
import { PREVIEW_HEIGHT, PREVIEW_WIDTH } from '../../utils'
|
||||
import { useSettings } from '../../hooks'
|
||||
|
||||
import './panel-component.scss'
|
||||
@@ -12,8 +11,8 @@ interface Props {
|
||||
isShown: boolean
|
||||
itemsCount: number
|
||||
setShown: () => void
|
||||
openSound: UIfx
|
||||
closeSound: UIfx
|
||||
openSoundPlay: (volume?: number) => void
|
||||
closeSoundPlay: (volume?: number) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -21,8 +20,8 @@ export const PanelComponent = ({
|
||||
isShown,
|
||||
setShown,
|
||||
children,
|
||||
openSound,
|
||||
closeSound,
|
||||
openSoundPlay,
|
||||
closeSoundPlay,
|
||||
itemsCount,
|
||||
orientation
|
||||
}: Props) => {
|
||||
@@ -49,9 +48,9 @@ export const PanelComponent = ({
|
||||
return
|
||||
}
|
||||
if (isShown) {
|
||||
openSound.play(UI_SOUND_VOLUME)
|
||||
openSoundPlay()
|
||||
} else {
|
||||
closeSound.play(UI_SOUND_VOLUME)
|
||||
closeSoundPlay()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import * as React from 'react'
|
||||
import UIfx from 'uifx'
|
||||
|
||||
import { useSettings } from '../../hooks'
|
||||
|
||||
import ru from '../../locales/ru.json'
|
||||
import en from '../../locales/en.json'
|
||||
import { useSettings } from '../../hooks'
|
||||
import { Settings } from '../../settings-context'
|
||||
import { CheckBoxComponent, SelectComponent } from '..'
|
||||
|
||||
import './settings-component.scss'
|
||||
import { Settings } from '../../settings-context'
|
||||
import { UI_SOUND_VOLUME } from '../../utils'
|
||||
import { CheckBoxComponent, SelectComponent } from '..'
|
||||
|
||||
type languageValue = keyof typeof ru;
|
||||
|
||||
interface Props {
|
||||
closeSettings: () => void
|
||||
checkboxOnSound: UIfx
|
||||
checkboxOffSound: UIfx
|
||||
checkboxOnSoundPlay: (volume?: number) => void
|
||||
checkboxOffSoundPlay: (volume?: number) => void
|
||||
}
|
||||
|
||||
export const SettingsComponent = ({closeSettings, checkboxOnSound, checkboxOffSound}: Props) => {
|
||||
export const SettingsComponent = ({closeSettings, checkboxOnSoundPlay, checkboxOffSoundPlay}: Props) => {
|
||||
const {settings, saveSettings} = useSettings()
|
||||
|
||||
const handleCheckboxClick = (option: keyof Settings) => {
|
||||
@@ -28,9 +25,9 @@ export const SettingsComponent = ({closeSettings, checkboxOnSound, checkboxOffSo
|
||||
return
|
||||
}
|
||||
if (settings[option]) {
|
||||
checkboxOffSound.play(UI_SOUND_VOLUME)
|
||||
checkboxOffSoundPlay()
|
||||
} else {
|
||||
checkboxOnSound.play(UI_SOUND_VOLUME)
|
||||
checkboxOnSoundPlay()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +43,7 @@ export const SettingsComponent = ({closeSettings, checkboxOnSound, checkboxOffSo
|
||||
if (!settings.uiSound) {
|
||||
return
|
||||
}
|
||||
checkboxOnSound.play(UI_SOUND_VOLUME)
|
||||
checkboxOnSoundPlay()
|
||||
}
|
||||
|
||||
const renderOption = (option: keyof Settings, value: boolean | string[] = []) => {
|
||||
|
||||
@@ -9,9 +9,45 @@
|
||||
min-height: 100vh;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
transition: background-image $transitionDuration $transitionType;
|
||||
animation: pulse 10s infinite;
|
||||
animation-direction: alternate;
|
||||
|
||||
&-background {
|
||||
z-index: 2;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
min-width: inherit;
|
||||
min-height: inherit;
|
||||
background: $backgroundTexture;
|
||||
transition: opacity $transitionDuration $transitionType;
|
||||
|
||||
&--loaded {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&-author {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
width: 110px;
|
||||
font-family: $font;
|
||||
text-shadow: $fontShadow;
|
||||
color: $fontColor;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
opacity: .3;
|
||||
|
||||
a {
|
||||
cursor: $cursorPointer, auto;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -12,19 +12,22 @@ interface Props {
|
||||
|
||||
export const ViewComponent = ({src}: Props) => {
|
||||
const [imageSrc, setImageSrc] = useState(Background)
|
||||
const [isLoaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoaded(false)
|
||||
const timer = setTimeout(() => {
|
||||
const image = new Image()
|
||||
image.src = src
|
||||
image.onload = () => {
|
||||
setImageSrc(src)
|
||||
setLoaded(true)
|
||||
}
|
||||
}, ANIMATION_DURATION)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [src])
|
||||
}, [src, imageSrc])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -32,7 +35,13 @@ export const ViewComponent = ({src}: Props) => {
|
||||
style={{
|
||||
backgroundImage: `url(${imageSrc})`
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<div className={`view-background ${isLoaded ? 'view-background--loaded' : ''}`}/>
|
||||
<div className='view-author'>
|
||||
<a href="https://github.com/obergodmar">obergodmar</a>
|
||||
<span>1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -5,5 +5,6 @@
|
||||
"ui.main-menu": "Main Menu",
|
||||
"ui.uiSound": "UI Sound",
|
||||
"ui.uiLanguage": "Language",
|
||||
"ui.language": "English"
|
||||
"ui.language": "English",
|
||||
"place.stormwind-park": "Stormwind Park"
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,5 +5,6 @@
|
||||
"ui.main-menu": "Главное меню",
|
||||
"ui.uiSound": "Звуки интерфейса",
|
||||
"ui.uiLanguage": "Язык",
|
||||
"ui.language": "Русский"
|
||||
"ui.language": "Русский",
|
||||
"place.stormwind-park": "Парк Штормграда"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
interface Config {
|
||||
volume?: number
|
||||
}
|
||||
|
||||
const name = 'UIAudio'
|
||||
|
||||
export default class Sound {
|
||||
public audio: HTMLAudioElement
|
||||
private volume: number
|
||||
|
||||
constructor(file: string, config?: Config) {
|
||||
const validateURI = (fileURI: string) => {
|
||||
if (fileURI) {
|
||||
return fileURI
|
||||
} else {
|
||||
throw Error('Requires valid URI path for "file"')
|
||||
}
|
||||
}
|
||||
|
||||
const volume = this.validateVolume(config && config.volume)
|
||||
|
||||
const appendAudioElement = (fileValue: string) => {
|
||||
const hashFn = (str: string) => {
|
||||
let hash = 0
|
||||
if (str.length === 0) {
|
||||
return hash
|
||||
}
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + char
|
||||
hash = hash & hash
|
||||
}
|
||||
return Math.abs(hash)
|
||||
}
|
||||
const id = `${name}-${hashFn(fileValue)}`
|
||||
const audioElement = document.createElement('audio')
|
||||
|
||||
audioElement.id = id
|
||||
audioElement.src = file
|
||||
audioElement.preload = 'auto'
|
||||
|
||||
document.body.appendChild(audioElement)
|
||||
return file
|
||||
}
|
||||
|
||||
const audioNode = appendAudioElement(validateURI(file))
|
||||
this.audio = new Audio(audioNode)
|
||||
this.audio.load()
|
||||
this.volume = volume
|
||||
}
|
||||
|
||||
public setVolume = (volume: number) => {
|
||||
this.validateVolume(volume)
|
||||
this.volume = volume
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public playSound = (volume: number = this.volume) => {
|
||||
this.audio.volume = this.validateVolume(volume)
|
||||
if (!this.audio.readyState) {
|
||||
return
|
||||
}
|
||||
this.audio.play().catch((error: Error) => console.error(`Error playback: ${error}`))
|
||||
}
|
||||
|
||||
public playMusic = (volume: number = this.volume) => {
|
||||
this.audio.volume = this.validateVolume(volume)
|
||||
if (!this.audio.readyState) {
|
||||
return
|
||||
}
|
||||
this.audio.play().catch((error: Error) => console.error(`Error playback: ${error}`))
|
||||
}
|
||||
|
||||
public pause = () => this.audio.pause()
|
||||
|
||||
private validateVolume = (volumeValue: number = 1.0) => {
|
||||
if (volumeValue && (volumeValue < 0 || volumeValue > 1)) {
|
||||
throw Error('"Volume" must be an number between 0.0 and 1.0')
|
||||
}
|
||||
return volumeValue
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/// <reference types="react-scripts" />
|
||||
declare module '*.ogg'
|
||||
declare module '*.mp3'
|
||||
@@ -3,3 +3,4 @@ export const LOADING_DURATION = 800
|
||||
export const PREVIEW_WIDTH = 320
|
||||
export const PREVIEW_HEIGHT = 180
|
||||
export const UI_SOUND_VOLUME = 0.2
|
||||
export const UI_MUSIC_VOLUME = 1
|
||||
|
||||
@@ -3,6 +3,7 @@ import { LOADING_DURATION } from './constants'
|
||||
export {
|
||||
PREVIEW_WIDTH,
|
||||
PREVIEW_HEIGHT,
|
||||
UI_MUSIC_VOLUME,
|
||||
UI_SOUND_VOLUME,
|
||||
LOADING_DURATION,
|
||||
ANIMATION_DURATION
|
||||
|
||||
Reference in New Issue
Block a user