fix loading
This commit is contained in:
142
src/App.jsx
142
src/App.jsx
@@ -6,6 +6,7 @@ import AvatarScene from './AvatarScene';
|
|||||||
import { LOCATIONS, teleportTo, worldState } from './worldState';
|
import { LOCATIONS, teleportTo, worldState } from './worldState';
|
||||||
import { adminMode, onAdminChange } from './controls/KeyInput';
|
import { adminMode, onAdminChange } from './controls/KeyInput';
|
||||||
import ControlPanel from './ui/ControlPanel';
|
import ControlPanel from './ui/ControlPanel';
|
||||||
|
import LoadingScreen from './ui/LoadingScreen';
|
||||||
|
|
||||||
const ACTION_SEQUENCE = ['attack', 'skill', 'jump', 'fly_dodge', 'damage'];
|
const ACTION_SEQUENCE = ['attack', 'skill', 'jump', 'fly_dodge', 'damage'];
|
||||||
const TELEPORT_ANIM = 'fly_dodge';
|
const TELEPORT_ANIM = 'fly_dodge';
|
||||||
@@ -13,7 +14,70 @@ const AUTO_INTERVAL_MIN = 15000;
|
|||||||
const AUTO_INTERVAL_MAX = 40000;
|
const AUTO_INTERVAL_MAX = 40000;
|
||||||
const NASA_URL = 'https://eyes.nasa.gov/apps/solar-system/#/earth?featured=false&detailPanel=false&logo=false&search=false&shareButton=false&menu=false&collapseSettingsOptions=true&hideFullScreenToggle=true';
|
const NASA_URL = 'https://eyes.nasa.gov/apps/solar-system/#/earth?featured=false&detailPanel=false&logo=false&search=false&shareButton=false&menu=false&collapseSettingsOptions=true&hideFullScreenToggle=true';
|
||||||
|
|
||||||
|
const BASE_URL = import.meta.env.BASE_URL;
|
||||||
|
const VRM_FILES = ['ai.vrm', 'ai_mode.vrm'];
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [loadProgress, setLoadProgress] = useState(0);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [fadeOut, setFadeOut] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const total = VRM_FILES.length;
|
||||||
|
let done = 0;
|
||||||
|
|
||||||
|
// Use XMLHttpRequest for progress tracking + populate browser cache
|
||||||
|
function loadFile(file) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', `${BASE_URL}model/${file}`, true);
|
||||||
|
xhr.responseType = 'arraybuffer';
|
||||||
|
xhr.onprogress = (e) => {
|
||||||
|
if (e.lengthComputable && !cancelled) {
|
||||||
|
const fileProgress = e.loaded / e.total;
|
||||||
|
setLoadProgress(((done + fileProgress) / total) * 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onload = () => {
|
||||||
|
done++;
|
||||||
|
if (!cancelled) setLoadProgress((done / total) * 100);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
xhr.onerror = () => { done++; resolve(); };
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load sequentially for accurate progress
|
||||||
|
loadFile(VRM_FILES[0])
|
||||||
|
.then(() => loadFile(VRM_FILES[1]))
|
||||||
|
.then(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setLoadProgress(100);
|
||||||
|
// Mount app behind loading screen, let it initialize
|
||||||
|
setTimeout(() => setLoaded(true), 300);
|
||||||
|
// Then fade out loading screen after app has time to render
|
||||||
|
setTimeout(() => setFadeOut(true), 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loaded && (
|
||||||
|
<div style={{ opacity: fadeOut ? 1 : 0, transition: 'opacity 0.8s ease' }}>
|
||||||
|
<AppMain />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<LoadingScreen progress={loadProgress} fadeOut={fadeOut} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppMain() {
|
||||||
const [animState, setAnimState] = useState({ name: 'fly_idle', count: 0 });
|
const [animState, setAnimState] = useState({ name: 'fly_idle', count: 0 });
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [view, setView] = useState('avatar'); // 'avatar' | 'nasa'
|
const [view, setView] = useState('avatar'); // 'avatar' | 'nasa'
|
||||||
@@ -29,7 +93,7 @@ export default function App() {
|
|||||||
const [scoreTimeLeft, setScoreTimeLeft] = useState(0);
|
const [scoreTimeLeft, setScoreTimeLeft] = useState(0);
|
||||||
const scoreEndRef = useRef(0);
|
const scoreEndRef = useRef(0);
|
||||||
const scoreRafRef = useRef(null);
|
const scoreRafRef = useRef(null);
|
||||||
const [crowns, setCrowns] = useState({ bronze: false, silver: false, gold: false });
|
const [crowns, setCrowns] = useState({ bronze: 0, silver: 0, gold: 0 });
|
||||||
const langRef = useRef('en');
|
const langRef = useRef('en');
|
||||||
const volumeRef = useRef(0);
|
const volumeRef = useRef(0);
|
||||||
const voiceIndexRef = useRef(0);
|
const voiceIndexRef = useRef(0);
|
||||||
@@ -201,9 +265,9 @@ export default function App() {
|
|||||||
scoreTimerRef.current = setTimeout(() => {
|
scoreTimerRef.current = setTimeout(() => {
|
||||||
setScore(prev => {
|
setScore(prev => {
|
||||||
setCrowns(c => ({
|
setCrowns(c => ({
|
||||||
bronze: c.bronze || prev >= 100,
|
bronze: (prev >= 100000 && !c.bronze) ? prev : c.bronze,
|
||||||
silver: c.silver || prev >= 100000,
|
silver: (prev >= 500000 && !c.silver) ? prev : c.silver,
|
||||||
gold: c.gold || prev >= 1000000,
|
gold: (prev >= 1000000 && !c.gold) ? prev : c.gold,
|
||||||
}));
|
}));
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
@@ -392,41 +456,43 @@ export default function App() {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
alignItems: 'center',
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
{crowns.gold > 0 && (
|
||||||
padding: '4px 8px',
|
<div style={{
|
||||||
background: crowns.gold ? 'rgba(255,255,255,0.1)' : 'transparent',
|
padding: '4px 8px',
|
||||||
border: crowns.gold ? '1px solid rgba(255,255,255,0.2)' : '1px solid transparent',
|
background: 'rgba(255,255,255,0.1)',
|
||||||
borderRadius: '4px',
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
borderRadius: '4px',
|
||||||
}}>
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
{crowns.gold && (
|
}}>
|
||||||
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="gold" style={{ width: 20, height: 20, filter: 'brightness(0) invert(1) sepia(1) saturate(10) hue-rotate(330deg)' }} />
|
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="gold" style={{ width: 16, height: 16, filter: 'brightness(0) invert(1) sepia(1) saturate(10) hue-rotate(330deg)' }} />
|
||||||
)}
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px', fontFamily: 'monospace' }}>{crowns.gold}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
)}
|
||||||
padding: '4px 8px',
|
{crowns.silver > 0 && (
|
||||||
background: crowns.silver ? 'rgba(255,255,255,0.1)' : 'transparent',
|
<div style={{
|
||||||
border: crowns.silver ? '1px solid rgba(255,255,255,0.2)' : '1px solid transparent',
|
padding: '4px 8px',
|
||||||
borderRadius: '4px',
|
background: 'rgba(255,255,255,0.1)',
|
||||||
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
}}>
|
borderRadius: '4px',
|
||||||
{crowns.silver && (
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="silver" style={{ width: 20, height: 20, filter: 'brightness(0) invert(1) brightness(0.9)' }} />
|
}}>
|
||||||
)}
|
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="silver" style={{ width: 16, height: 16, filter: 'brightness(0) invert(1) brightness(0.9)' }} />
|
||||||
</div>
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px', fontFamily: 'monospace' }}>{crowns.silver}</span>
|
||||||
<div style={{
|
</div>
|
||||||
padding: '4px 8px',
|
)}
|
||||||
background: crowns.bronze ? 'rgba(255,255,255,0.1)' : 'transparent',
|
{crowns.bronze > 0 && (
|
||||||
border: crowns.bronze ? '1px solid rgba(255,255,255,0.2)' : '1px solid transparent',
|
<div style={{
|
||||||
borderRadius: '4px',
|
padding: '4px 8px',
|
||||||
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
background: 'rgba(255,255,255,0.1)',
|
||||||
}}>
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
{crowns.bronze && (
|
borderRadius: '4px',
|
||||||
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="bronze" style={{ width: 20, height: 20, filter: 'brightness(0) invert(1) sepia(1) saturate(10) hue-rotate(90deg)' }} />
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
)}
|
}}>
|
||||||
</div>
|
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="bronze" style={{ width: 16, height: 16, filter: 'brightness(0) invert(1) sepia(1) saturate(10) hue-rotate(90deg)' }} />
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px', fontFamily: 'monospace' }}>{crowns.bronze}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ function trimClip(clip, duration, addLoopPose) {
|
|||||||
return clip;
|
return clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preload both VRM models (parse + cache in useLoader)
|
||||||
|
const VRM_MODELS = ['ai.vrm', 'ai_mode.vrm'];
|
||||||
|
VRM_MODELS.forEach(model => {
|
||||||
|
useLoader.preload(GLTFLoader, `${BASE_URL}model/${model}`, (loader) => {
|
||||||
|
loader.register((parser) => new VRMLoaderPlugin(parser));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export default function VrmCharacter({ selectedAnimation: animState, vrmModel = 'ai.vrm' }) {
|
export default function VrmCharacter({ selectedAnimation: animState, vrmModel = 'ai.vrm' }) {
|
||||||
const selectedAnimation = animState?.name || animState;
|
const selectedAnimation = animState?.name || animState;
|
||||||
const mixerRef = useRef(null);
|
const mixerRef = useRef(null);
|
||||||
|
|||||||
56
src/ui/LoadingScreen.jsx
Normal file
56
src/ui/LoadingScreen.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
background: '#000',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
transition: 'opacity 0.8s ease',
|
||||||
|
};
|
||||||
|
|
||||||
|
const barOuterStyle = {
|
||||||
|
width: '240px',
|
||||||
|
height: '2px',
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '1px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginTop: 16,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle = {
|
||||||
|
color: 'rgba(255,255,255,0.4)',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
letterSpacing: '2px',
|
||||||
|
marginTop: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoadingScreen({ progress, fadeOut }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
...containerStyle,
|
||||||
|
opacity: fadeOut ? 0 : 1,
|
||||||
|
pointerEvents: fadeOut ? 'none' : 'auto',
|
||||||
|
}}>
|
||||||
|
<div style={barOuterStyle}>
|
||||||
|
<div style={{
|
||||||
|
width: `${progress}%`,
|
||||||
|
height: '100%',
|
||||||
|
background: 'rgba(255,255,255,0.6)',
|
||||||
|
borderRadius: '1px',
|
||||||
|
transition: 'width 0.3s ease',
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
<div style={labelStyle}>
|
||||||
|
{progress < 100 ? `${Math.floor(progress)}%` : 'READY'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user