|
1 | 1 | import * as React from "react"; |
2 | 2 | import * as THREE from "three"; |
3 | | -import { Environment, useProgress } from "@react-three/drei"; |
4 | 3 | import { GithubLogo } from "./Ui/GithubLogo"; |
5 | | -import { Canvas } from "@react-three/fiber"; |
| 4 | +import { Canvas, useFrame, useThree } from "@react-three/fiber"; |
6 | 5 | import { XR8Controls } from "../XR8Canvas/XR8Controls"; |
7 | 6 | import { useXR8 } from "../XR8Canvas/useXR8"; |
8 | | -import { xr8Hosted } from "../XR8Canvas/getXR8"; |
| 7 | +import { getXR8, loadXR8, xr8Hosted } from "../XR8Canvas/getXR8"; |
9 | 8 | import { Game } from "./Game"; |
10 | 9 | import { Dice } from "./Scene/Dice"; |
11 | 10 | // @ts-ignore |
12 | 11 | import { Visualizer } from "react-touch-visualizer"; |
13 | 12 | import tunnel from "tunnel-rat"; |
14 | 13 | import { Ground } from "./Scene/Ground"; |
| 14 | +import { WebXRControls } from "../WebXRCanvas/WebXRControls"; |
15 | 15 | import { createPortal } from "react-dom"; |
| 16 | +import { XR8 } from "../XR8Canvas/XR8"; |
| 17 | +import { Environment } from "./Scene/Environment"; |
| 18 | +import { TrackingHint } from "./Ui/Hints/TrackingHint"; |
| 19 | +import { useProgress } from "@react-three/drei"; |
| 20 | +import { useIsWebXRSupported } from "../WebXRCanvas/useWebXRSession"; |
| 21 | +import { useDelay } from "./Ui/useDelay"; |
| 22 | +import { PageRules } from "./Ui/PageRules"; |
| 23 | +import { LoadingScreen } from "./Ui/LoadingScreen"; |
16 | 24 |
|
17 | 25 | // @ts-ignore |
18 | 26 | const xr8ApiKey: string | undefined = import.meta.env.VITE_XR8_API_KEY; |
19 | 27 | const touchSupported = "ontouchend" in document; |
20 | 28 |
|
21 | | -type Props = { |
22 | | - started: boolean; |
23 | | - onReady: () => void; |
24 | | - onProgress?: (x: number, label: string) => void; |
25 | | -}; |
| 29 | +export const App = () => { |
| 30 | + const [state, setState] = React.useState< |
| 31 | + | { type: "loading" } |
| 32 | + | { type: "waiting-user-input" } |
| 33 | + | { |
| 34 | + type: "webXR"; |
| 35 | + poseFound?: boolean; |
| 36 | + cameraFeedDisplayed?: boolean; |
| 37 | + webXRSession?: XRSession; |
| 38 | + } |
| 39 | + | { |
| 40 | + type: "xr8"; |
| 41 | + poseFound?: boolean; |
| 42 | + cameraFeedDisplayed?: boolean; |
| 43 | + xr8?: XR8; |
| 44 | + } |
| 45 | + | { type: "flat" } |
| 46 | + >({ type: "waiting-user-input" }); |
| 47 | + |
| 48 | + const uiTunnel = React.useMemo(tunnel, []); |
26 | 49 |
|
27 | | -export const App = ({ onReady, onProgress, started }: Props) => { |
28 | 50 | const [error, setError] = React.useState<Error>(); |
29 | 51 | if (error) throw error; |
30 | 52 |
|
31 | | - const [xr8Ready, setXr8Ready] = React.useState(false); |
| 53 | + const startWebXR = () => { |
| 54 | + setState({ type: "webXR" }); |
32 | 55 |
|
33 | | - const xr8Supported = (xr8ApiKey || xr8Hosted) && touchSupported; |
| 56 | + // this call must be made after a user input |
| 57 | + return navigator.xr |
| 58 | + ?.requestSession("immersive-ar", { |
| 59 | + optionalFeatures: ["dom-overlay", "local-floor"], |
| 60 | + domOverlay: { root: document.getElementById("overlay")! }, |
| 61 | + }) |
| 62 | + .then((webXRSession) => |
| 63 | + setState({ |
| 64 | + type: "webXR", |
| 65 | + webXRSession, |
| 66 | + }) |
| 67 | + ) |
| 68 | + .catch(setError); |
| 69 | + }; |
34 | 70 |
|
35 | | - const xr8 = xr8Supported ? useXR8(xr8ApiKey) : null; |
| 71 | + const startXR8 = () => { |
| 72 | + setState({ type: "xr8" }); |
| 73 | + loadXR8(xr8ApiKey) |
| 74 | + .then((xr8) => setState({ type: "xr8", xr8 })) |
| 75 | + .catch(setError); |
| 76 | + }; |
36 | 77 |
|
37 | | - const { active, progress, total } = useProgress(); |
38 | | - const ready = (!xr8Supported || xr8Ready) && !active; |
| 78 | + const startFlat = () => setState({ type: "flat" }); |
39 | 79 |
|
40 | | - let progressValue = total > 1 ? progress / 100 : 0; |
41 | | - let progressLabel = "loading assets"; |
| 80 | + const webXRSupported = useIsWebXRSupported(); |
42 | 81 |
|
43 | | - if (xr8Supported) { |
44 | | - if (progressValue < 1) { |
45 | | - progressValue *= 0.6; |
46 | | - } else { |
47 | | - if (!xr8) { |
48 | | - progressValue = 0.6; |
49 | | - progressLabel = "loading xr8 library"; |
50 | | - } else { |
51 | | - progressValue = 0.8; |
52 | | - progressLabel = "tracking in progress"; |
53 | | - } |
54 | | - } |
55 | | - } |
| 82 | + const xr8Supported = (!!xr8ApiKey || xr8Hosted) && touchSupported; |
56 | 83 |
|
57 | | - React.useEffect( |
58 | | - () => void onProgress?.(progressValue, progressLabel), |
59 | | - [progressValue, progressLabel] |
60 | | - ); |
61 | | - React.useEffect(() => void (ready && onReady()), [ready]); |
| 84 | + const sceneAssetLoaded = useProgress(({ active }) => !active); |
62 | 85 |
|
63 | | - const uiTunnel = React.useMemo(tunnel, []); |
| 86 | + const readyForRender = |
| 87 | + sceneAssetLoaded && |
| 88 | + (state.type === "flat" || |
| 89 | + (state.type === "webXR" && state.cameraFeedDisplayed) || |
| 90 | + (state.type === "xr8" && state.cameraFeedDisplayed)); |
| 91 | + |
| 92 | + const readyForGame = |
| 93 | + readyForRender && |
| 94 | + !(state.type === "webXR" && !state.poseFound) && |
| 95 | + !(state.type === "xr8" && !state.cameraFeedDisplayed); |
| 96 | + |
| 97 | + const hint = useDelay(readyForRender && !readyForGame && "tracking", 2500); |
| 98 | + |
| 99 | + if (webXRSupported === "loading") return null; |
| 100 | + |
| 101 | + if (state.type === "loading") return null; |
64 | 102 |
|
65 | 103 | return ( |
66 | 104 | <> |
67 | | - {false && <Visualizer />} |
68 | | - |
69 | 105 | <Canvas |
70 | | - camera={{ position: new THREE.Vector3(0, 6, 6) }} |
| 106 | + camera={{ position: new THREE.Vector3(0, 6, 6), near: 0.1, far: 1000 }} |
71 | 107 | shadows |
72 | 108 | style={{ |
73 | 109 | position: "fixed", |
74 | 110 | top: 0, |
75 | 111 | left: 0, |
76 | 112 | right: 0, |
77 | 113 | bottom: 0, |
78 | | - opacity: started ? 1 : 0, |
79 | 114 | touchAction: "none", |
| 115 | + opacity: readyForRender ? 1 : 0, |
80 | 116 | }} |
81 | 117 | > |
82 | | - <ErrorBoundary onError={setError}> |
83 | | - {xr8 && <XR8Controls xr8={xr8} onReady={() => setXr8Ready(true)} />} |
84 | | - |
85 | | - <React.Suspense fallback={null}> |
86 | | - <Environment path={"assets/"} files={"lebombo_1k.hdr"} /> |
87 | | - |
88 | | - {started && <Game UiPortal={uiTunnel.In} />} |
89 | | - |
90 | | - {active && <Dice value={1} /> /* ensure the model is loaded */} |
91 | | - </React.Suspense> |
92 | | - |
93 | | - <directionalLight position={[10, 8, 6]} intensity={0} castShadow /> |
94 | | - |
95 | | - <Ground /> |
96 | | - </ErrorBoundary> |
| 118 | + {state.type === "xr8" && state.xr8 && ( |
| 119 | + <XR8Controls |
| 120 | + xr8={state.xr8} |
| 121 | + onPoseFound={() => setState((s) => ({ ...s, poseFound: true }))} |
| 122 | + onCameraFeedDisplayed={() => |
| 123 | + setState((s) => ({ ...s, cameraFeedDisplayed: true })) |
| 124 | + } |
| 125 | + /> |
| 126 | + )} |
| 127 | + |
| 128 | + {state.type === "webXR" && state.webXRSession && ( |
| 129 | + <WebXRControls |
| 130 | + worldSize={8} |
| 131 | + webXRSession={state.webXRSession} |
| 132 | + onPoseFound={() => setState((s) => ({ ...s, poseFound: true }))} |
| 133 | + onCameraFeedDisplayed={() => |
| 134 | + setState((s) => ({ ...s, cameraFeedDisplayed: true })) |
| 135 | + } |
| 136 | + /> |
| 137 | + )} |
| 138 | + |
| 139 | + <React.Suspense fallback={null}> |
| 140 | + <Environment /> |
| 141 | + |
| 142 | + { |
| 143 | + /* preload the dice model */ |
| 144 | + !readyForGame && ( |
| 145 | + <Dice |
| 146 | + position={[999, 999, 9999]} |
| 147 | + scale={[0.0001, 0.0001, 0.0001]} |
| 148 | + /> |
| 149 | + ) |
| 150 | + } |
| 151 | + |
| 152 | + {readyForGame && <Game UiPortal={uiTunnel.In} />} |
| 153 | + </React.Suspense> |
| 154 | + |
| 155 | + <directionalLight position={[10, 8, 6]} intensity={0} castShadow /> |
| 156 | + |
| 157 | + <Ground /> |
97 | 158 | </Canvas> |
98 | 159 |
|
99 | | - {createPortal( |
100 | | - <> |
101 | | - <a href="https://github.com/platane/yAR-htzee" title="github"> |
102 | | - <button |
103 | | - style={{ |
104 | | - position: "absolute", |
105 | | - width: "40px", |
106 | | - height: "40px", |
107 | | - bottom: "10px", |
108 | | - right: "10px", |
109 | | - pointerEvents: "auto", |
110 | | - zIndex: 1, |
111 | | - }} |
112 | | - > |
113 | | - <GithubLogo /> |
114 | | - </button> |
115 | | - </a> |
116 | | - |
117 | | - {React.createElement(uiTunnel.Out)} |
118 | | - </>, |
119 | | - document.getElementById("overlay")! |
120 | | - )} |
| 160 | + <OverlayPortal> |
| 161 | + {false && <Visualizer />} |
| 162 | + |
| 163 | + <a href="https://github.com/platane/yAR-htzee" title="github"> |
| 164 | + <button |
| 165 | + style={{ |
| 166 | + position: "absolute", |
| 167 | + width: "40px", |
| 168 | + height: "40px", |
| 169 | + bottom: "10px", |
| 170 | + right: "10px", |
| 171 | + pointerEvents: "auto", |
| 172 | + zIndex: 1, |
| 173 | + }} |
| 174 | + > |
| 175 | + <GithubLogo /> |
| 176 | + </button> |
| 177 | + </a> |
| 178 | + |
| 179 | + {React.createElement(uiTunnel.Out)} |
| 180 | + |
| 181 | + {hint === "tracking" && <TrackingHint />} |
| 182 | + |
| 183 | + {!readyForRender && ( |
| 184 | + <Over> |
| 185 | + <LoadingScreen |
| 186 | + loading={state.type !== "waiting-user-input"} |
| 187 | + onStartFlat={startFlat} |
| 188 | + onStartWebXR={webXRSupported && startWebXR} |
| 189 | + onStartXR8={xr8Supported && startXR8} |
| 190 | + /> |
| 191 | + </Over> |
| 192 | + )} |
| 193 | + </OverlayPortal> |
121 | 194 | </> |
122 | 195 | ); |
123 | 196 | }; |
124 | 197 |
|
125 | | -class ErrorBoundary extends React.Component<{ |
126 | | - onError: (error: Error) => void; |
127 | | - children?: any; |
128 | | -}> { |
129 | | - static getDerivedStateFromError = (error: Error) => ({ error }); |
130 | | - |
131 | | - state: { error?: Error } = {}; |
132 | | - |
133 | | - componentDidCatch(error: Error) { |
134 | | - this.props.onError(error); |
135 | | - } |
136 | | - |
137 | | - render() { |
138 | | - if (this.state.error) return null; |
139 | | - return this.props.children; |
140 | | - } |
141 | | -} |
142 | | - |
143 | | -export default App; |
| 198 | +const OverlayPortal = ({ children }: { children?: any }) => |
| 199 | + createPortal(children, document.getElementById("overlay")!); |
| 200 | + |
| 201 | +const Over = ({ children }: { children?: any }) => ( |
| 202 | + <div |
| 203 | + style={{ |
| 204 | + position: "fixed", |
| 205 | + top: 0, |
| 206 | + left: 0, |
| 207 | + width: "100%", |
| 208 | + height: "100%", |
| 209 | + backgroundColor: "white", |
| 210 | + }} |
| 211 | + > |
| 212 | + {children} |
| 213 | + </div> |
| 214 | +); |
0 commit comments