feat(aklive2d): performance optimization for showcase and error page for directory

This commit is contained in:
Haoyu Xu
2023-03-04 01:05:36 -05:00
parent 1c0f425c9a
commit 2fde26d96e
24 changed files with 352 additions and 148 deletions

View File

@@ -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"}

View File

@@ -1 +1 @@
0.5.30
1.0.4

View File

@@ -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
})
}

View 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;
}
}

View File

@@ -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>
);
}

View File

@@ -9,6 +9,7 @@
align-items: flex-start;
gap: 0.5rem;
padding-left: 1rem;
word-break: break-word;
}
.changelogs .item-info-content {

View File

@@ -3,6 +3,7 @@
display: flex;
align-items: flex-end;
flex-wrap: wrap;
user-select: none;
}
.item-group-date {

View File

@@ -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 {

View File

@@ -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">

View File

@@ -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;

View File

@@ -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 }) {