feat(aklive2d): performance optimization for showcase and error page for directory
This commit is contained in:
@@ -2,4 +2,6 @@ VITE_APP_TITLE=AKLive2D
|
||||
VITE_APP_VOICE_URL=jp/CN_037.ogg
|
||||
VITE_VOICE_FOLDERS={"main":"voice","sub":[{"name":"jp","lang":"JP"},{"name":"cn","lang":"CN_MANDARIN"},{"name":"en","lang":"EN"},{"name":"kr","lang":"KR"},{"name":"custom","lang":"CUSTOM"}]}
|
||||
VITE_DIRECTORY_FOLDER="_assets"
|
||||
VITE_BACKGROUND_FOLDER="background"
|
||||
VITE_BACKGROUND_FOLDER="background"
|
||||
VITE_AVAILABLE_OPERATORS=["chen","dusk","dusk_everything_is_a_miracle","ling","nearl","nian","nian_unfettered_freedom","phatom_focus","rosmontis","skadi","skadi_sublimation","w","w_wonder","specter","gavial","surtr_colorful_wonderland","lee_trust_your_eyes","texas_the_omertosa","nearl_relight","rosmontis_become_anew","passager_dream_in_a_moment","mizuki_summer_feast","chongyue","ling_it_does_wash_the_strings","pozemka_snowy_plains_in_words"]
|
||||
VITE_ERROR_FILES={"files":[{"key":"build_char_128_plosis_epoque%233","paddings":{"left":-120,"right":150,"top":10,"bottom":0}},{"key":"build_char_128_plosis","paddings":{"left":-90,"right":100,"top":10,"bottom":0}}],"voice":"CN_034.ogg"}
|
||||
@@ -1 +1 @@
|
||||
0.5.30
|
||||
1.0.4
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "react"
|
||||
import { useCallback } from "react"
|
||||
const audioEl = new Audio()
|
||||
|
||||
let lastSrc = ''
|
||||
export default function useAudio() {
|
||||
const [isPlaying, _setIsPlaying] = useState(false)
|
||||
const isPlayingRef = useRef(isPlaying)
|
||||
@@ -29,17 +29,19 @@ export default function useAudio() {
|
||||
},
|
||||
callback = () => { }
|
||||
) => {
|
||||
if (!options.overwrite && audioEl.src === (window.location.href.replace(/\/$/g, '') + link)) return
|
||||
if (!options.overwrite && link === lastSrc) return
|
||||
audioEl.src = link
|
||||
let startPlayPromise = audioEl.play()
|
||||
if (startPlayPromise !== undefined) {
|
||||
setIsPlaying(true)
|
||||
startPlayPromise
|
||||
.then(() => {
|
||||
lastSrc = link
|
||||
callback()
|
||||
return
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
53
directory/src/routes/error-page.css
Normal file
53
directory/src/routes/error-page.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.error-page .header {
|
||||
padding: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.error-page .main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding-top: 10rem;
|
||||
font-size: 3rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.error-page .spine {
|
||||
max-width: 600px;
|
||||
flex: 1;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.error-page .spine.active {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-page .main {
|
||||
padding-top: 6rem;
|
||||
}
|
||||
.error-page .content {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.error-page .main {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
.error-page .content {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,170 @@
|
||||
import React from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useCallback
|
||||
} from "react";
|
||||
import {
|
||||
useNavigate,
|
||||
useRouteError
|
||||
} from "react-router-dom";
|
||||
import './error-page.css'
|
||||
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 useAudio from '@/libs/voice';
|
||||
import spine from '!/libs/spine-player'
|
||||
import '!/libs/spine-player.css'
|
||||
|
||||
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
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error = useRouteError();
|
||||
const navigate = useNavigate();
|
||||
console.log(error)
|
||||
const {
|
||||
setTitle,
|
||||
} = useHeader()
|
||||
const [voiceOn, setVoiceOn] = useAtom(voiceOnAtom)
|
||||
const [spineDone, _setSpineDone] = useState(false)
|
||||
const spineRef = useRef(null)
|
||||
const [spineData, setSpineData] = useState(null)
|
||||
const { play, stop } = useAudio()
|
||||
const spineDoneRef = useRef(spineDone)
|
||||
const voiceOnRef = useRef(voiceOn)
|
||||
|
||||
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])
|
||||
stop()
|
||||
}, [content, setTitle, stop])
|
||||
|
||||
useEffect(() => {
|
||||
if (!voiceOn) {
|
||||
stop()
|
||||
}
|
||||
}, [voiceOn, stop])
|
||||
|
||||
useEffect(() => {
|
||||
voiceOnRef.current = voiceOn
|
||||
}, [voiceOn])
|
||||
|
||||
const playVoice = useCallback(() => {
|
||||
play(`/${import.meta.env.VITE_DIRECTORY_FOLDER}/error.ogg`, { overwrite: true })
|
||||
}, [play])
|
||||
|
||||
useEffect(() => {
|
||||
if (voiceOn) playVoice()
|
||||
}, [playVoice, voiceOn])
|
||||
|
||||
useEffect(() => {
|
||||
if (spineRef.current?.children.length === 0 && spineData) {
|
||||
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,
|
||||
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>
|
||||
error
|
||||
<button onClick={() => navigate(-1, { replace: true })}>Go Home</button>
|
||||
<section className='error-page'>
|
||||
<header className='header'>
|
||||
<ReturnButton
|
||||
className='return-button'
|
||||
onClick={() => navigate(-1, { replace: true })}
|
||||
/>
|
||||
<Switch
|
||||
key="voice"
|
||||
text='voice'
|
||||
on={voiceOn}
|
||||
handleOnClick={() => setVoiceOn(!voiceOn)}
|
||||
/>
|
||||
</header>
|
||||
<main className='main'>
|
||||
{
|
||||
content.map((item, index) => {
|
||||
return (
|
||||
<section key={index} className='content'>
|
||||
<Typewriter
|
||||
words={[item]}
|
||||
cursor
|
||||
cursorStyle='|'
|
||||
typeSpeed={100}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
})
|
||||
}
|
||||
<section
|
||||
className={`spine ${spineDone ? 'active' : ''}`}
|
||||
ref={spineRef}
|
||||
/>
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.changelogs .item-info-content {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.item-group-date {
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.operator .spine-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.operator .spine-container:before {
|
||||
content: "";
|
||||
display: block;
|
||||
@@ -28,9 +27,9 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.operator .spine-settings,
|
||||
.operator .steam-workshop-wrapper {
|
||||
.operator .spine-settings {
|
||||
margin-right: 1.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.operator .text {
|
||||
|
||||
@@ -33,8 +33,6 @@ const getVoiceFoler = (lang) => {
|
||||
}
|
||||
const spinePlayerAtom = atom(null);
|
||||
const spineAnimationAtom = atom("Idle");
|
||||
const voiceLangAtom = atom(null);
|
||||
const subtitleLangAtom = atom(null);
|
||||
|
||||
const getTabName = (item, language) => {
|
||||
if (item.type === 'operator') {
|
||||
@@ -63,11 +61,11 @@ export default function Operator() {
|
||||
const [spineAnimation, setSpineAnimation] = useAtom(spineAnimationAtom)
|
||||
const { i18n } = useI18n()
|
||||
const [spinePlayer, setSpinePlayer] = useAtom(spinePlayerAtom)
|
||||
const [voiceLang, _setVoiceLang] = useAtom(voiceLangAtom)
|
||||
const [voiceLang, _setVoiceLang] = useState(null)
|
||||
const { backgrounds } = useBackgrounds()
|
||||
const [currentBackground, setCurrentBackground] = useState(null)
|
||||
const [voiceConfig, setVoiceConfig] = useState(null)
|
||||
const [subtitleLang, setSubtitleLang] = useAtom(subtitleLangAtom)
|
||||
const [subtitleLang, setSubtitleLang] = useState(null)
|
||||
const [hideSubtitle, setHideSubtitle] = useState(true)
|
||||
const { play, stop, getSrc, isPlaying, isPlayingRef } = useAudio()
|
||||
const [subtitleObj, _setSubtitleObj] = useState(null)
|
||||
@@ -339,6 +337,10 @@ export default function Operator() {
|
||||
}
|
||||
]
|
||||
|
||||
if (!JSON.parse(import.meta.env.VITE_AVAILABLE_OPERATORS).includes(key)) {
|
||||
throw new Error('Operator not found')
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="operator">
|
||||
<section className="spine-player-wrapper">
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drawer .links {
|
||||
@@ -237,6 +238,10 @@
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.footer .section {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 1rem 0;
|
||||
|
||||
@@ -132,6 +132,7 @@ export default function Root() {
|
||||
function FooterElement() {
|
||||
const { i18n } = useI18n()
|
||||
const { version } = useConfig()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
@@ -160,7 +161,9 @@ function FooterElement() {
|
||||
</Popup>
|
||||
</section>
|
||||
</section>
|
||||
<section className='copyright section'>
|
||||
<section className='copyright section' onDoubleClick={() => {
|
||||
navigate('/error')
|
||||
}}>
|
||||
<span>Spine Runtimes © 2013 - 2019 Esoteric Software LLC</span>
|
||||
<span>Assets © 2017 - {currentYear} Arknights/Hypergryph Co., Ltd</span>
|
||||
<span>Source Code © 2021 - {currentYear} Halyul</span>
|
||||
@@ -169,7 +172,7 @@ function FooterElement() {
|
||||
</section>
|
||||
</footer>
|
||||
)
|
||||
}, [i18n, version.directory, version.showcase])
|
||||
}, [i18n, navigate, version.directory, version.showcase])
|
||||
}
|
||||
|
||||
function DrawerDestinations({ toggleDrawer }) {
|
||||
|
||||
Reference in New Issue
Block a user