add keyboard overlay for non-touch devices

This commit is contained in:
Brian Beck 2026-02-12 17:54:51 -08:00
parent 0aadb3d6e7
commit d7ef09c576
26 changed files with 215 additions and 107 deletions

View file

@ -4,12 +4,17 @@ import { Canvas, GLProps } from "@react-three/fiber";
import { NoToneMapping, SRGBColorSpace, PCFShadowMap, Camera } from "three";
import { Mission } from "@/src/components/Mission";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ObserverControls } from "@/src/components/ObserverControls";
import {
ObserverControls,
KEYBOARD_CONTROLS,
} from "@/src/components/ObserverControls";
import { KeyboardOverlay } from "@/src/components/KeyboardOverlay";
import {
TouchJoystick,
TouchCameraMovement,
type JoystickState,
} from "@/src/components/TouchControls";
import { KeyboardControls } from "@react-three/drei";
import { InspectorControls } from "@/src/components/InspectorControls";
import { useTouchDevice } from "@/src/components/useTouchDevice";
import { SettingsProvider } from "@/src/components/SettingsProvider";
@ -130,68 +135,71 @@ function MapInspector() {
<QueryClientProvider client={queryClient}>
<main>
<SettingsProvider>
<div id="canvasContainer">
{showLoadingIndicator && (
<div id="loadingIndicator" data-complete={!isLoading}>
<div className="LoadingSpinner" />
<div className="LoadingProgress">
<div
className="LoadingProgress-bar"
style={{ width: `${loadingProgress * 100}%` }}
/>
</div>
<div className="LoadingProgress-text">
{Math.round(loadingProgress * 100)}%
</div>
</div>
)}
<Canvas
frameloop="always"
gl={glSettings}
shadows={{ type: PCFShadowMap }}
onCreated={(state) => {
cameraRef.current = state.camera;
}}
>
<CamerasProvider>
<AudioProvider>
<Mission
key={`${missionName}~${missionType}`}
name={missionName}
missionType={missionType}
onLoadingChange={handleLoadingChange}
/>
<ObserverCamera />
<DebugElements />
{isTouch === null ? null : isTouch ? (
<TouchCameraMovement
joystickState={joystickStateRef}
joystickZone={joystickZoneRef}
lookJoystickState={lookJoystickStateRef}
lookJoystickZone={lookJoystickZoneRef}
<KeyboardControls map={KEYBOARD_CONTROLS}>
<div id="canvasContainer">
{showLoadingIndicator && (
<div id="loadingIndicator" data-complete={!isLoading}>
<div className="LoadingSpinner" />
<div className="LoadingProgress">
<div
className="LoadingProgress-bar"
style={{ width: `${loadingProgress * 100}%` }}
/>
) : (
<ObserverControls />
)}
</AudioProvider>
</CamerasProvider>
</Canvas>
</div>
{isTouch && (
<TouchJoystick
joystickState={joystickStateRef}
joystickZone={joystickZoneRef}
lookJoystickState={lookJoystickStateRef}
lookJoystickZone={lookJoystickZoneRef}
</div>
<div className="LoadingProgress-text">
{Math.round(loadingProgress * 100)}%
</div>
</div>
)}
<Canvas
frameloop="always"
gl={glSettings}
shadows={{ type: PCFShadowMap }}
onCreated={(state) => {
cameraRef.current = state.camera;
}}
>
<CamerasProvider>
<AudioProvider>
<Mission
key={`${missionName}~${missionType}`}
name={missionName}
missionType={missionType}
onLoadingChange={handleLoadingChange}
/>
<ObserverCamera />
<DebugElements />
{isTouch === null ? null : isTouch ? (
<TouchCameraMovement
joystickState={joystickStateRef}
joystickZone={joystickZoneRef}
lookJoystickState={lookJoystickStateRef}
lookJoystickZone={lookJoystickZoneRef}
/>
) : (
<ObserverControls />
)}
</AudioProvider>
</CamerasProvider>
</Canvas>
</div>
{isTouch && (
<TouchJoystick
joystickState={joystickStateRef}
joystickZone={joystickZoneRef}
lookJoystickState={lookJoystickStateRef}
lookJoystickZone={lookJoystickZoneRef}
/>
)}
{isTouch === false && <KeyboardOverlay />}
<InspectorControls
missionName={missionName}
missionType={missionType}
onChangeMission={changeMission}
cameraRef={cameraRef}
isTouch={isTouch}
/>
)}
<InspectorControls
missionName={missionName}
missionType={missionType}
onChangeMission={changeMission}
cameraRef={cameraRef}
isTouch={isTouch}
/>
</KeyboardControls>
</SettingsProvider>
</main>
</QueryClientProvider>

View file

@ -513,6 +513,62 @@ input[type="range"] {
}
}
.KeyboardOverlay {
position: fixed;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: flex-end;
gap: 10px;
pointer-events: none;
z-index: 1;
}
.KeyboardOverlay-column {
display: flex;
gap: 4px;
flex-direction: column;
justify-content: center;
}
.KeyboardOverlay-row {
display: flex;
gap: 4px;
justify-content: stretch;
}
.KeyboardOverlay-spacer {
width: 32px;
}
.KeyboardOverlay-key {
min-width: 32px;
height: 32px;
display: flex;
flex: 1 0 0;
align-items: center;
justify-content: center;
padding: 0 8px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.5);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.KeyboardOverlay-key[data-pressed="true"] {
background: rgba(52, 187, 171, 0.6);
border-color: rgba(35, 253, 220, 0.5);
color: #fff;
}
.KeyboardOverlay-arrow {
margin-right: 3px;
}
.TouchJoystick {
position: fixed;
bottom: 20px;