191 lines
5.5 KiB
JavaScript
191 lines
5.5 KiB
JavaScript
import React, {
|
|
useState,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useCallback
|
|
} from "react";
|
|
import {
|
|
useNavigate,
|
|
useRouteError
|
|
} from "react-router-dom";
|
|
import header from '@/scss/root/header.module.scss'
|
|
import classes from '@/scss/error/Error.module.scss'
|
|
import { useAtom } from 'jotai'
|
|
import { atomWithStorage } from 'jotai/utils';
|
|
import Switch from '@/component/switch';
|
|
import ReturnButton from "@/component/return_button";
|
|
import { Typewriter } from 'react-simple-typewriter'
|
|
import { useHeader } from '@/state/header';
|
|
import VoiceElement from '@/component/voice';
|
|
import spine from '!/libs/spine-player'
|
|
import '!/libs/spine-player.css'
|
|
import useUmami from '@/state/insights';
|
|
|
|
const voiceOnAtom = atomWithStorage('voiceOn', false)
|
|
const config = JSON.parse(import.meta.env.VITE_ERROR_FILES)
|
|
const obj = config.files[Math.floor((Math.random() * config.files.length))]
|
|
const filename = obj.key.replace("#", "%23")
|
|
const padding = obj.paddings
|
|
let lastVoiceState = 'ended'
|
|
|
|
export default function Error() {
|
|
// eslint-disable-next-line no-unused-vars
|
|
const _trackEvt = useUmami('/error')
|
|
const error = useRouteError();
|
|
const navigate = useNavigate();
|
|
const {
|
|
setTitle,
|
|
} = useHeader()
|
|
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
|
|
const [spineDone, _setSpineDone] = useState(false)
|
|
const spineRef = useRef(null)
|
|
const [spineData, setSpineData] = useState(null)
|
|
const spineDoneRef = useRef(spineDone)
|
|
const voiceOnRef = useRef(voiceOn)
|
|
const [voiceSrc, setVoiceSrc] = useState(null)
|
|
const [voiceReplay, setVoiceReplay] = useState(false)
|
|
const [spinePlayer, setSpinePlayer] = useState(null)
|
|
|
|
const setSpineDone = (data) => {
|
|
spineDoneRef.current = data
|
|
_setSpineDone(data)
|
|
}
|
|
|
|
const content = useMemo(() => ['エラー発生。', '发生错误。', 'Error occured.', '에러 발생.', '發生錯誤。'], [])
|
|
|
|
useEffect(() => {
|
|
console.log(error)
|
|
fetch(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/${filename}.json`).then(res => res.json()).then(data => {
|
|
setSpineData(data)
|
|
})
|
|
}, [error])
|
|
|
|
useEffect(() => {
|
|
setTitle(content[0])
|
|
}, [content, setTitle])
|
|
|
|
useEffect(() => {
|
|
if (!voiceOn) {
|
|
setVoiceSrc(null)
|
|
} else {
|
|
setVoiceSrc(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/error.ogg`)
|
|
if (spinePlayer) {
|
|
spinePlayer.animationState.setAnimation(0, "Interact", false, 0);
|
|
spinePlayer.animationState.addAnimation(0, "Relax", true, 0);
|
|
}
|
|
}
|
|
}, [voiceOn])
|
|
|
|
useEffect(() => {
|
|
voiceOnRef.current = voiceOn
|
|
}, [voiceOn])
|
|
|
|
const playVoice = useCallback(() => {
|
|
if (lastVoiceState === 'ended' && voiceSrc !== null) {
|
|
setVoiceReplay(true)
|
|
}
|
|
}, [voiceSrc])
|
|
|
|
const handleAduioStateChange = useCallback((e, state) => {
|
|
lastVoiceState = state
|
|
if (state === 'ended') {
|
|
setVoiceReplay(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (spineRef.current?.children.length === 0 && spineData) {
|
|
setSpinePlayer(new spine.SpinePlayer(spineRef.current, {
|
|
skelUrl: `./assets/${filename}.skel`,
|
|
atlasUrl: `./assets/${filename}.atlas`,
|
|
rawDataURIs: spineData,
|
|
animation: 'Relax',
|
|
premultipliedAlpha: true,
|
|
alpha: true,
|
|
backgroundColor: "#00000000",
|
|
viewport: {
|
|
debugRender: false,
|
|
padLeft: `${padding.left}%`,
|
|
padRight: `${padding.right}%`,
|
|
padTop: `${padding.top}%`,
|
|
padBottom: `${padding.bottom}%`,
|
|
x: 0,
|
|
y: 0,
|
|
},
|
|
showControls: false,
|
|
touch: false,
|
|
fps: 60,
|
|
defaultMix: 0.3,
|
|
success: (player) => {
|
|
let isPlayingInteract = false
|
|
player.animationState.addListener({
|
|
end: (e) => {
|
|
if (e.animation.name == "Interact") {
|
|
isPlayingInteract = false;
|
|
}
|
|
}
|
|
});
|
|
setSpineDone(true)
|
|
const ani = () => {
|
|
if (isPlayingInteract) {
|
|
return;
|
|
}
|
|
isPlayingInteract = true;
|
|
player.animationState.setAnimation(0, "Interact", false, 0);
|
|
player.animationState.addAnimation(0, "Relax", true, 0);
|
|
if (voiceOnRef.current) playVoice()
|
|
}
|
|
ani()
|
|
player.canvas.onclick = () => {
|
|
ani()
|
|
}
|
|
player.canvas.onmouseenter = () => {
|
|
ani()
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}, [playVoice, spineData]);
|
|
|
|
return (
|
|
<section className={classes.error}>
|
|
<header className={`${header.header} ${classes.header}`}>
|
|
<ReturnButton
|
|
onClick={() => navigate(-1, { replace: true })}
|
|
/>
|
|
<Switch
|
|
key="voice"
|
|
text='voice'
|
|
on={voiceOn}
|
|
handleOnClick={() => setVoiceOn(!voiceOn)}
|
|
/>
|
|
</header>
|
|
<main className={classes.main}>
|
|
{
|
|
content.map((item, index) => {
|
|
return (
|
|
<section key={index} className={classes.content}>
|
|
<Typewriter
|
|
words={[item]}
|
|
cursor
|
|
cursorStyle='|'
|
|
typeSpeed={100}
|
|
/>
|
|
</section>
|
|
)
|
|
})
|
|
}
|
|
<section
|
|
className={`${classes.spine} ${spineDone ? classes.active : ''}`}
|
|
ref={spineRef}
|
|
/>
|
|
<VoiceElement
|
|
src={voiceSrc}
|
|
replay={voiceReplay}
|
|
handleAduioStateChange={handleAduioStateChange}
|
|
/>
|
|
</main>
|
|
</section>
|
|
);
|
|
} |