277 lines
8.8 KiB
HTML
277 lines
8.8 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8" />
|
||
|
|
<title>VRM AI - WebXR</title>
|
||
|
|
<meta
|
||
|
|
name="viewport"
|
||
|
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||
|
|
/>
|
||
|
|
<style>
|
||
|
|
* {
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||
|
|
Roboto, Oxygen, Ubuntu, sans-serif;
|
||
|
|
background-color: #1a1a2e;
|
||
|
|
color: #eee;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
canvas {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* VR Button Styling Override */
|
||
|
|
#VRButton {
|
||
|
|
font-family: inherit !important;
|
||
|
|
font-weight: 600 !important;
|
||
|
|
letter-spacing: 0.5px !important;
|
||
|
|
border-radius: 8px !important;
|
||
|
|
transition: all 0.3s ease !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
#VRButton:hover {
|
||
|
|
transform: translateX(-50%) scale(1.05) !important;
|
||
|
|
box-shadow: 0 4px 20px rgba(0, 150, 255, 0.4) !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Status Overlay */
|
||
|
|
#status-overlay {
|
||
|
|
position: fixed;
|
||
|
|
top: 10px;
|
||
|
|
left: 10px;
|
||
|
|
z-index: 1000;
|
||
|
|
background: rgba(0, 0, 0, 0.7);
|
||
|
|
padding: 10px 15px;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-size: 12px;
|
||
|
|
pointer-events: none;
|
||
|
|
opacity: 0.8;
|
||
|
|
transition: opacity 0.3s;
|
||
|
|
}
|
||
|
|
|
||
|
|
#status-overlay:hover {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
#status-overlay .status-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
margin: 4px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
#status-overlay .status-dot {
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
border-radius: 50%;
|
||
|
|
margin-right: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
#status-overlay .status-dot.connected {
|
||
|
|
background: #4caf50;
|
||
|
|
box-shadow: 0 0 6px #4caf50;
|
||
|
|
}
|
||
|
|
|
||
|
|
#status-overlay .status-dot.disconnected {
|
||
|
|
background: #f44336;
|
||
|
|
box-shadow: 0 0 6px #f44336;
|
||
|
|
}
|
||
|
|
|
||
|
|
#status-overlay .status-dot.recording {
|
||
|
|
background: #ff5722;
|
||
|
|
box-shadow: 0 0 6px #ff5722;
|
||
|
|
animation: pulse 0.8s infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes pulse {
|
||
|
|
0%,
|
||
|
|
100% {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
50% {
|
||
|
|
opacity: 0.4;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Instructions Panel */
|
||
|
|
#instructions {
|
||
|
|
position: fixed;
|
||
|
|
bottom: 80px;
|
||
|
|
left: 50%;
|
||
|
|
transform: translateX(-50%);
|
||
|
|
z-index: 999;
|
||
|
|
background: rgba(0, 0, 0, 0.8);
|
||
|
|
padding: 15px 25px;
|
||
|
|
border-radius: 10px;
|
||
|
|
text-align: center;
|
||
|
|
font-size: 13px;
|
||
|
|
max-width: 400px;
|
||
|
|
opacity: 0.9;
|
||
|
|
transition: opacity 0.3s;
|
||
|
|
}
|
||
|
|
|
||
|
|
#instructions h3 {
|
||
|
|
margin: 0 0 10px 0;
|
||
|
|
font-size: 15px;
|
||
|
|
color: #64b5f6;
|
||
|
|
}
|
||
|
|
|
||
|
|
#instructions p {
|
||
|
|
margin: 5px 0;
|
||
|
|
line-height: 1.4;
|
||
|
|
}
|
||
|
|
|
||
|
|
#instructions .key {
|
||
|
|
display: inline-block;
|
||
|
|
background: #333;
|
||
|
|
padding: 2px 8px;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-family: monospace;
|
||
|
|
margin: 0 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Hide instructions when in VR (body gets class) */
|
||
|
|
body.vr-active #instructions,
|
||
|
|
body.vr-active #status-overlay {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Loading Screen */
|
||
|
|
#loading {
|
||
|
|
position: fixed;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
background: #1a1a2e;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
justify-content: center;
|
||
|
|
align-items: center;
|
||
|
|
z-index: 2000;
|
||
|
|
transition: opacity 0.5s;
|
||
|
|
}
|
||
|
|
|
||
|
|
#loading.hidden {
|
||
|
|
opacity: 0;
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
#loading .spinner {
|
||
|
|
width: 50px;
|
||
|
|
height: 50px;
|
||
|
|
border: 3px solid #333;
|
||
|
|
border-top-color: #64b5f6;
|
||
|
|
border-radius: 50%;
|
||
|
|
animation: spin 1s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes spin {
|
||
|
|
to {
|
||
|
|
transform: rotate(360deg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#loading p {
|
||
|
|
margin-top: 20px;
|
||
|
|
font-size: 14px;
|
||
|
|
color: #888;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<!-- Loading Screen -->
|
||
|
|
<div id="loading">
|
||
|
|
<div class="spinner"></div>
|
||
|
|
<p>Loading VRM Model...</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Status Overlay -->
|
||
|
|
<div id="status-overlay">
|
||
|
|
<div class="status-item">
|
||
|
|
<span class="status-dot disconnected" id="ws-status"></span>
|
||
|
|
<span id="ws-text">WebSocket: Connecting...</span>
|
||
|
|
</div>
|
||
|
|
<div class="status-item">
|
||
|
|
<span class="status-dot disconnected" id="mic-status"></span>
|
||
|
|
<span id="mic-text">Microphone: Ready</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Instructions Panel -->
|
||
|
|
<div id="instructions">
|
||
|
|
<h3>🎮 VR Controls</h3>
|
||
|
|
<p></p>
|
||
|
|
<span class="key">Grip Button</span> Hold to talk (Push-to-Talk)
|
||
|
|
</p>
|
||
|
|
<p><span class="key">Trigger</span> Interact / Point</p>
|
||
|
|
<p>Click "Enter VR" to start your session</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script type="importmap">
|
||
|
|
{
|
||
|
|
"imports": {
|
||
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
|
||
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/",
|
||
|
|
"@pixiv/three-vrm": "https://cdn.jsdelivr.net/npm/@pixiv/three-vrm@3.4.1/lib/three-vrm.module.js",
|
||
|
|
"@pixiv/three-vrm-animation": "https://cdn.jsdelivr.net/npm/@pixiv/three-vrm-animation@3.4.1/lib/three-vrm-animation.module.js"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<script type="module" src="./main-VR.js"></script>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// Simple status updates from main-VR.js events
|
||
|
|
window.addEventListener("vrm-loaded", () => {
|
||
|
|
document.getElementById("loading").classList.add("hidden");
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener("ws-connected", () => {
|
||
|
|
document.getElementById("ws-status").className =
|
||
|
|
"status-dot connected";
|
||
|
|
document.getElementById("ws-text").textContent =
|
||
|
|
"WebSocket: Connected";
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener("ws-disconnected", () => {
|
||
|
|
document.getElementById("ws-status").className =
|
||
|
|
"status-dot disconnected";
|
||
|
|
document.getElementById("ws-text").textContent =
|
||
|
|
"WebSocket: Disconnected";
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener("recording-start", () => {
|
||
|
|
document.getElementById("mic-status").className =
|
||
|
|
"status-dot recording";
|
||
|
|
document.getElementById("mic-text").textContent =
|
||
|
|
"Microphone: Recording...";
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener("recording-stop", () => {
|
||
|
|
document.getElementById("mic-status").className =
|
||
|
|
"status-dot connected";
|
||
|
|
document.getElementById("mic-text").textContent =
|
||
|
|
"Microphone: Ready";
|
||
|
|
});
|
||
|
|
|
||
|
|
// Handle VR session state
|
||
|
|
if (navigator.xr) {
|
||
|
|
navigator.xr
|
||
|
|
.isSessionSupported("immersive-vr")
|
||
|
|
.then((supported) => {
|
||
|
|
if (!supported) {
|
||
|
|
document.getElementById("instructions").innerHTML =
|
||
|
|
"<h3>⚠️ VR Not Supported</h3><p>WebXR is not available on this device/browser.</p><p>Try using a VR headset or compatible browser.</p>";
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|