diff --git a/docs/poster.pdf b/docs/poster.pdf new file mode 100644 index 0000000..e8c162f Binary files /dev/null and b/docs/poster.pdf differ diff --git a/package-lock.json b/package-lock.json index 19db788..70664b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "react-slick": "^0.30.3", "slick-carousel": "^1.8.1", "tailwindcss": "^4.1.11", - "three": "^0.178.0" + "three": "^0.178.0", + "webxr-polyfill": "^2.0.3" }, "devDependencies": { "@eslint/js": "^9.29.0", @@ -29,6 +30,7 @@ "@types/react-dom": "^19.1.6", "@types/react-slick": "^0.23.13", "@types/three": "^0.178.1", + "@types/webxr": "^0.5.22", "@vitejs/plugin-react": "^4.5.2", "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", @@ -2963,6 +2965,17 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cardboard-vr-display": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/cardboard-vr-display/-/cardboard-vr-display-1.0.19.tgz", + "integrity": "sha512-+MjcnWKAkb95p68elqZLDPzoiF/dGncQilLGvPBM5ZorABp/ao3lCs7nnRcYBckmuNkg1V/5rdGDKoUaCVsHzQ==", + "license": "Apache-2.0", + "dependencies": { + "gl-preserve-state": "^1.0.0", + "nosleep.js": "^0.7.0", + "webvr-polyfill-dpdb": "^1.0.17" + } + }, "node_modules/case-anything": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", @@ -4025,6 +4038,12 @@ "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", "license": "MIT" }, + "node_modules/gl-preserve-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz", + "integrity": "sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4785,6 +4804,12 @@ "dev": true, "license": "MIT" }, + "node_modules/nosleep.js": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.7.0.tgz", + "integrity": "sha512-Z4B1HgvzR+en62ghwZf6BwAR6x4/pjezsiMcbF9KMLh7xoscpoYhaSXfY3lLkqC68AtW+/qLJ1lzvBIj0FGaTA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5927,6 +5952,28 @@ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", "license": "MIT" }, + "node_modules/webvr-polyfill-dpdb": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/webvr-polyfill-dpdb/-/webvr-polyfill-dpdb-1.0.18.tgz", + "integrity": "sha512-O0S1ZGEWyPvyZEkS2VbyV7mtir/NM9MNK3EuhbHPoJ8EHTky2pTXehjIl+IiDPr+Lldgx129QGt3NGly7rwRPw==", + "license": "Apache-2.0" + }, + "node_modules/webxr-polyfill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/webxr-polyfill/-/webxr-polyfill-2.0.3.tgz", + "integrity": "sha512-lgTKYVeD4HeTwWdJN+SeS6iflGx3epz/3dww9X4GyuuXmYGAV5p8l34jUM/HRGHn1jKS3oZNZRC/J9MlSk/Zhg==", + "license": "Apache-2.0", + "dependencies": { + "cardboard-vr-display": "^1.0.19", + "gl-matrix": "^2.8.1" + } + }, + "node_modules/webxr-polyfill/node_modules/gl-matrix": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-2.8.1.tgz", + "integrity": "sha512-0YCjVpE3pS5XWlN3J4X7AiAx65+nqAI54LndtVFnQZB6G/FVLkZH8y8V6R3cIoOQR4pUdfwQGd1iwyoXHJ4Qfw==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 84b0992..a70d588 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "react-slick": "^0.30.3", "slick-carousel": "^1.8.1", "tailwindcss": "^4.1.11", - "three": "^0.178.0" + "three": "^0.178.0", + "webxr-polyfill": "^2.0.3" }, "devDependencies": { "@eslint/js": "^9.29.0", @@ -31,6 +32,7 @@ "@types/react-dom": "^19.1.6", "@types/react-slick": "^0.23.13", "@types/three": "^0.178.1", + "@types/webxr": "^0.5.22", "@vitejs/plugin-react": "^4.5.2", "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/src/components/DreamVR.tsx b/src/components/DreamVR.tsx index 4413c0e..ddea9ef 100644 --- a/src/components/DreamVR.tsx +++ b/src/components/DreamVR.tsx @@ -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; - }; -} // 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(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 ( - - ); - } - - 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 = ({dream, height = '500px'}) => { - const [isFullscreen, setIsFullscreen] = useState(false); - const [useDeviceOrientation, setUseDeviceOrientation] = useState(false); - const [deviceOrientationPermission, setDeviceOrientationPermission] = useState<'granted' | 'denied' | 'unknown'>('unknown'); const containerRef = useRef(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 = ({dream, height = '500px'}) => { return (
- {/* Controls */} + {/* VR Entry Button */}
- - {deviceOrientationPermission === 'unknown' && ( - - )} - - {deviceOrientationPermission === 'granted' && ( - - )}
- + - {/* Camera setup */} - + + {/* Camera setup */} + - {/* Controls - either OrbitControls or DeviceOrientationControls */} - + {/* Lighting */} + + + - {/* Lighting */} - - - + {/* Neural network visualization */} + - {/* Neural network visualization */} - - - {/* Background */} - + {/* Background */} + +
);