introduced WebXR support by integrating webxr-polyfill and refactoring DreamVR component for enhanced VR functionality and simplified code structure

Signed-off-by: Matthias Puchstein <matthias@puchstein.bayern>
This commit is contained in:
2025-07-17 03:22:19 +02:00
parent 8b0996781e
commit 2edf6b3f1f
4 changed files with 89 additions and 195 deletions

View File

@@ -1,18 +1,9 @@
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {Canvas, useFrame, useThree} from '@react-three/fiber';
import {OrbitControls, PerspectiveCamera} from '@react-three/drei';
import React, {useMemo, useRef} from 'react';
import {Canvas, useFrame} from '@react-three/fiber';
import {createXRStore, XR} from '@react-three/xr';
import {PerspectiveCamera} from '@react-three/drei';
import * as THREE from 'three';
import type Dream from '../types/Dream';
import {DeviceOrientationControls} from 'three-stdlib';
// Extended window interface with DeviceOrientationEvent
interface WindowWithDeviceOrientation extends Window {
DeviceOrientationEvent: {
prototype: DeviceOrientationEvent;
new(type: string, eventInitDict?: DeviceOrientationEventInit): DeviceOrientationEvent;
requestPermission?: () => Promise<string>;
};
}
// Neural Node component representing a synapse in the neural network
const NeuralNode = ({position, color, scale, pulseSpeed, pulseIntensity}: {
@@ -224,137 +215,18 @@ const NeuralNetwork = ({dream}: { dream: Dream }) => {
);
};
// Custom controls component that switches between OrbitControls and DeviceOrientationControls
const Controls = ({useDeviceOrientation}: { useDeviceOrientation: boolean }) => {
const {camera} = useThree();
const controlsRef = useRef<DeviceOrientationControls | null>(null);
useEffect(() => {
if (useDeviceOrientation) {
// Create DeviceOrientationControls
controlsRef.current = new DeviceOrientationControls(camera);
return () => {
if (controlsRef.current) {
controlsRef.current.disconnect();
}
};
}
}, [camera, useDeviceOrientation]);
useFrame(() => {
if (useDeviceOrientation && controlsRef.current) {
controlsRef.current.update();
}
});
// If not using device orientation, use OrbitControls
if (!useDeviceOrientation) {
return (
<OrbitControls
enableZoom={true}
enablePan={true}
enableRotate={true}
autoRotate={true}
autoRotateSpeed={0.5}
/>
);
}
return null;
};
// Main DreamVR component
interface DreamVRProps {
dream: Dream;
height?: string;
}
// Create XR store outside the component to avoid recreation on each render
const store = createXRStore();
const DreamVR: React.FC<DreamVRProps> = ({dream, height = '500px'}) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const [useDeviceOrientation, setUseDeviceOrientation] = useState(false);
const [deviceOrientationPermission, setDeviceOrientationPermission] = useState<'granted' | 'denied' | 'unknown'>('unknown');
const containerRef = useRef<HTMLDivElement>(null);
// Function to toggle fullscreen
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
// Enter fullscreen
if (containerRef.current?.requestFullscreen) {
containerRef.current.requestFullscreen()
.then(() => setIsFullscreen(true))
.catch(err => console.error(`Error attempting to enable fullscreen: ${err.message}`));
}
} else {
// Exit fullscreen
if (document.exitFullscreen) {
document.exitFullscreen()
.then(() => setIsFullscreen(false))
.catch(err => console.error(`Error attempting to exit fullscreen: ${err.message}`));
}
}
};
// Function to request device orientation permission
const requestDeviceOrientationPermission = () => {
// Cast window to our extended interface
const windowWithOrientation = window as unknown as WindowWithDeviceOrientation;
// Check if DeviceOrientationEvent is available and if requestPermission is a function
if (typeof windowWithOrientation.DeviceOrientationEvent !== 'undefined' &&
typeof windowWithOrientation.DeviceOrientationEvent.requestPermission === 'function') {
// iOS 13+ requires permission
windowWithOrientation.DeviceOrientationEvent.requestPermission()
.then((permissionState: string) => {
if (permissionState === 'granted') {
setDeviceOrientationPermission('granted');
setUseDeviceOrientation(true);
} else {
setDeviceOrientationPermission('denied');
setUseDeviceOrientation(false);
}
})
.catch((error: Error) => {
console.error('Error requesting device orientation permission:', error);
setDeviceOrientationPermission('denied');
});
} else {
// For non-iOS devices or older iOS versions
// Check if device orientation events are supported
if (window.DeviceOrientationEvent) {
setDeviceOrientationPermission('granted');
setUseDeviceOrientation(true);
} else {
setDeviceOrientationPermission('denied');
}
}
};
// Effect to handle fullscreen change
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
// Check if device is mobile
useEffect(() => {
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
// For mobile devices, we'll show the device orientation button
setDeviceOrientationPermission('unknown');
} else {
// For desktop, we'll use orbit controls
setDeviceOrientationPermission('denied');
}
}, []);
// Only render VR for dream with chip input type
if (dream.input.inputType !== 'chip') {
return (
@@ -368,74 +240,47 @@ const DreamVR: React.FC<DreamVRProps> = ({dream, height = '500px'}) => {
return (
<div ref={containerRef} style={{height, width: '100%', position: 'relative'}}>
{/* Controls */}
{/* VR Entry Button */}
<div className="absolute top-2 right-2 z-10 flex space-x-2">
<button
onClick={toggleFullscreen}
onClick={() => store.enterVR()}
className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors"
aria-label={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
aria-label="Enter VR"
>
{isFullscreen ? (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path
d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path
d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
</svg>
)}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</button>
{deviceOrientationPermission === 'unknown' && (
<button
onClick={requestDeviceOrientationPermission}
className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors"
aria-label="Enable Device Orientation"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<line x1="12" y1="18" x2="12" y2="18"/>
</svg>
</button>
)}
{deviceOrientationPermission === 'granted' && (
<button
onClick={() => setUseDeviceOrientation(!useDeviceOrientation)}
className={`p-2 backdrop-blur-sm rounded-full text-white transition-colors ${useDeviceOrientation ? 'bg-white/40' : 'bg-white/20 hover:bg-white/30'}`}
aria-label={useDeviceOrientation ? "Disable Device Orientation" : "Enable Device Orientation"}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<path d="M12 18h.01"/>
</svg>
</button>
)}
</div>
<Canvas shadows>
{/* Camera setup */}
<PerspectiveCamera makeDefault position={[0, 0, 30]}/>
<XR store={store}>
{/* Camera setup */}
<PerspectiveCamera makeDefault position={[0, 0, 30]}/>
{/* Controls - either OrbitControls or DeviceOrientationControls */}
<Controls useDeviceOrientation={useDeviceOrientation}/>
{/* Lighting */}
<ambientLight intensity={0.5}/>
<directionalLight position={[10, 10, 10]} intensity={1}/>
<directionalLight position={[-10, -10, -10]} intensity={0.5}/>
{/* Lighting */}
<ambientLight intensity={0.5}/>
<pointLight position={[10, 10, 10]} intensity={1}/>
<pointLight position={[-10, -10, -10]} intensity={0.5}/>
{/* Neural network visualization */}
<NeuralNetwork dream={dream}/>
{/* Neural network visualization */}
<NeuralNetwork dream={dream}/>
{/* Background */}
<color attach="background" args={['#000']}/>
{/* Background */}
<color attach="background" args={['#000']}/>
</XR>
</Canvas>
</div>
);