Add mobile touch controls: virtual joystick, look drag, and Y-axis buttons
This commit is contained in:
Generated
+1
@@ -2206,6 +2206,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"getrandom",
|
||||
"glam",
|
||||
"js-sys",
|
||||
"log",
|
||||
"pollster",
|
||||
"rand",
|
||||
|
||||
@@ -22,6 +22,7 @@ console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Document",
|
||||
"Window",
|
||||
|
||||
+338
-23
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>wgpu demo</title>
|
||||
<link data-trunk rel="rust" data-wasm-opt="0" />
|
||||
<style>
|
||||
@@ -12,10 +13,12 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
touch-action: none;
|
||||
}
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#help-overlay {
|
||||
@@ -118,6 +121,15 @@
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Hide desktop controls info on touch devices */
|
||||
@media (pointer: coarse) {
|
||||
.desktop-controls { display: none !important; }
|
||||
}
|
||||
/* Hide mobile controls info on desktop */
|
||||
@media (pointer: fine) {
|
||||
.mobile-controls { display: none !important; }
|
||||
}
|
||||
|
||||
#coord-bar {
|
||||
position: fixed;
|
||||
bottom: 25%;
|
||||
@@ -167,37 +179,136 @@
|
||||
#coord-bar .coord-unit {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* Touch controls */
|
||||
#touch-controls {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
#touch-controls { display: block; }
|
||||
}
|
||||
|
||||
#joystick-base {
|
||||
position: absolute;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#joystick-thumb {
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.y-btn {
|
||||
position: absolute;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.y-btn.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
#y-up-btn {
|
||||
right: 20px;
|
||||
bottom: 130px;
|
||||
}
|
||||
|
||||
#y-down-btn {
|
||||
right: 20px;
|
||||
bottom: 60px;
|
||||
}
|
||||
|
||||
/* Compact coord bar on mobile */
|
||||
@media (pointer: coarse) {
|
||||
#coord-bar .coord-inner {
|
||||
font-size: 11px;
|
||||
padding: 4px 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="help-overlay">
|
||||
<div class="help-box">
|
||||
<h2>Controls</h2>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Move (view-relative)</span>
|
||||
<span class="help-keys"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></span>
|
||||
<div class="desktop-controls">
|
||||
<div class="help-row">
|
||||
<span class="help-action">Move (view-relative)</span>
|
||||
<span class="help-keys"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Move (axis-aligned)</span>
|
||||
<span class="help-keys"><kbd>Shift</kbd>+<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Up / Down (Z)</span>
|
||||
<span class="help-keys"><kbd>E</kbd><kbd>Q</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Look</span>
|
||||
<span class="help-keys"><kbd>Mouse</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Release Cursor</span>
|
||||
<span class="help-keys"><kbd>Esc</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Toggle Help</span>
|
||||
<span class="help-keys"><kbd>H</kbd></span>
|
||||
</div>
|
||||
<div class="help-dismiss">Click anywhere to start · Press <kbd>H</kbd> to toggle this help</div>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Move (axis-aligned)</span>
|
||||
<span class="help-keys"><kbd>Shift</kbd>+<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></span>
|
||||
<div class="mobile-controls">
|
||||
<div class="help-row">
|
||||
<span class="help-action">Move (X/Z)</span>
|
||||
<span class="help-keys"><kbd>Left stick</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Up / Down (Y)</span>
|
||||
<span class="help-keys"><kbd>Buttons</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Look</span>
|
||||
<span class="help-keys"><kbd>Right drag</kbd></span>
|
||||
</div>
|
||||
<div class="help-dismiss">Tap anywhere to start</div>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Up / Down (Z)</span>
|
||||
<span class="help-keys"><kbd>E</kbd><kbd>Q</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Look</span>
|
||||
<span class="help-keys"><kbd>Mouse</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Release Cursor</span>
|
||||
<span class="help-keys"><kbd>Esc</kbd></span>
|
||||
</div>
|
||||
<div class="help-row">
|
||||
<span class="help-action">Toggle Help</span>
|
||||
<span class="help-keys"><kbd>H</kbd></span>
|
||||
</div>
|
||||
<div class="help-dismiss">Click anywhere to start · Press <kbd>H</kbd> to toggle this help</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -208,6 +319,13 @@
|
||||
|
||||
<div id="scanner-separator"></div>
|
||||
|
||||
<div id="touch-controls">
|
||||
<div id="joystick-base"></div>
|
||||
<div id="joystick-thumb"></div>
|
||||
<button id="y-up-btn" class="y-btn">▲</button>
|
||||
<button id="y-down-btn" class="y-btn">▼</button>
|
||||
</div>
|
||||
|
||||
<div id="coord-bar">
|
||||
<div class="coord-inner">
|
||||
<span><span class="coord-label">X</span><span id="coord-x">10.0</span></span>
|
||||
@@ -220,5 +338,202 @@
|
||||
<span id="move-mode">VIEW</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Touch state shared with WASM via window._touchState
|
||||
var state = {
|
||||
moveX: 0, moveZ: 0,
|
||||
lookDX: 0, lookDY: 0,
|
||||
yUp: false, yDown: false
|
||||
};
|
||||
window._touchState = state;
|
||||
|
||||
// Only set up touch controls on touch devices
|
||||
if (!('ontouchstart' in window)) return;
|
||||
|
||||
var joystickBase = document.getElementById('joystick-base');
|
||||
var joystickThumb = document.getElementById('joystick-thumb');
|
||||
var yUpBtn = document.getElementById('y-up-btn');
|
||||
var yDownBtn = document.getElementById('y-down-btn');
|
||||
|
||||
var JOYSTICK_MAX_RADIUS = 50;
|
||||
var joystickTouchId = null;
|
||||
var joystickCenterX = 0;
|
||||
var joystickCenterY = 0;
|
||||
|
||||
var lookTouchId = null;
|
||||
var lookLastX = 0;
|
||||
var lookLastY = 0;
|
||||
|
||||
// Dismiss help overlay on first touch
|
||||
var helpDismissed = false;
|
||||
|
||||
function showJoystick(cx, cy) {
|
||||
joystickBase.style.left = cx + 'px';
|
||||
joystickBase.style.top = cy + 'px';
|
||||
joystickBase.style.display = 'block';
|
||||
joystickThumb.style.left = cx + 'px';
|
||||
joystickThumb.style.top = cy + 'px';
|
||||
joystickThumb.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideJoystick() {
|
||||
joystickBase.style.display = 'none';
|
||||
joystickThumb.style.display = 'none';
|
||||
}
|
||||
|
||||
function updateJoystickThumb(x, y) {
|
||||
joystickThumb.style.left = x + 'px';
|
||||
joystickThumb.style.top = y + 'px';
|
||||
}
|
||||
|
||||
function isOverYButton(el, x, y) {
|
||||
var r = el.getBoundingClientRect();
|
||||
return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
// Dismiss help on first touch
|
||||
if (!helpDismissed) {
|
||||
helpDismissed = true;
|
||||
var overlay = document.getElementById('help-overlay');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
}
|
||||
|
||||
for (var i = 0; i < e.changedTouches.length; i++) {
|
||||
var touch = e.changedTouches[i];
|
||||
var x = touch.clientX;
|
||||
var y = touch.clientY;
|
||||
var screenW = window.innerWidth;
|
||||
|
||||
// Skip if touch is on a Y button (handled separately)
|
||||
if (isOverYButton(yUpBtn, x, y) || isOverYButton(yDownBtn, x, y)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (x < screenW * 0.45) {
|
||||
// Left side: joystick
|
||||
if (joystickTouchId === null) {
|
||||
joystickTouchId = touch.identifier;
|
||||
joystickCenterX = x;
|
||||
joystickCenterY = y;
|
||||
showJoystick(x, y);
|
||||
state.moveX = 0;
|
||||
state.moveZ = 0;
|
||||
}
|
||||
} else {
|
||||
// Right side: look
|
||||
if (lookTouchId === null) {
|
||||
lookTouchId = touch.identifier;
|
||||
lookLastX = x;
|
||||
lookLastY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default to avoid scrolling/zooming (but not on buttons)
|
||||
if (e.target !== yUpBtn && e.target !== yDownBtn) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
document.addEventListener('touchmove', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
for (var i = 0; i < e.changedTouches.length; i++) {
|
||||
var touch = e.changedTouches[i];
|
||||
|
||||
if (touch.identifier === joystickTouchId) {
|
||||
var dx = touch.clientX - joystickCenterX;
|
||||
var dy = touch.clientY - joystickCenterY;
|
||||
var dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist > JOYSTICK_MAX_RADIUS) {
|
||||
dx = dx / dist * JOYSTICK_MAX_RADIUS;
|
||||
dy = dy / dist * JOYSTICK_MAX_RADIUS;
|
||||
dist = JOYSTICK_MAX_RADIUS;
|
||||
}
|
||||
|
||||
// Normalize to -1..1
|
||||
state.moveX = dx / JOYSTICK_MAX_RADIUS;
|
||||
state.moveZ = dy / JOYSTICK_MAX_RADIUS;
|
||||
|
||||
// Update visual
|
||||
updateJoystickThumb(
|
||||
joystickCenterX + dx,
|
||||
joystickCenterY + dy
|
||||
);
|
||||
}
|
||||
|
||||
if (touch.identifier === lookTouchId) {
|
||||
var ldx = touch.clientX - lookLastX;
|
||||
var ldy = touch.clientY - lookLastY;
|
||||
state.lookDX += ldx;
|
||||
state.lookDY += ldy;
|
||||
lookLastX = touch.clientX;
|
||||
lookLastY = touch.clientY;
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
function handleTouchEnd(e) {
|
||||
for (var i = 0; i < e.changedTouches.length; i++) {
|
||||
var touch = e.changedTouches[i];
|
||||
|
||||
if (touch.identifier === joystickTouchId) {
|
||||
joystickTouchId = null;
|
||||
state.moveX = 0;
|
||||
state.moveZ = 0;
|
||||
hideJoystick();
|
||||
}
|
||||
|
||||
if (touch.identifier === lookTouchId) {
|
||||
lookTouchId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('touchend', handleTouchEnd, { passive: false });
|
||||
document.addEventListener('touchcancel', handleTouchEnd, { passive: false });
|
||||
|
||||
// Y buttons
|
||||
yUpBtn.addEventListener('touchstart', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
state.yUp = true;
|
||||
yUpBtn.classList.add('active');
|
||||
}, { passive: false });
|
||||
|
||||
yUpBtn.addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
state.yUp = false;
|
||||
yUpBtn.classList.remove('active');
|
||||
}, { passive: false });
|
||||
|
||||
yUpBtn.addEventListener('touchcancel', function(e) {
|
||||
state.yUp = false;
|
||||
yUpBtn.classList.remove('active');
|
||||
}, { passive: false });
|
||||
|
||||
yDownBtn.addEventListener('touchstart', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
state.yDown = true;
|
||||
yDownBtn.classList.add('active');
|
||||
}, { passive: false });
|
||||
|
||||
yDownBtn.addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
state.yDown = false;
|
||||
yDownBtn.classList.remove('active');
|
||||
}, { passive: false });
|
||||
|
||||
yDownBtn.addEventListener('touchcancel', function(e) {
|
||||
state.yDown = false;
|
||||
yDownBtn.classList.remove('active');
|
||||
}, { passive: false });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,6 +4,20 @@ use winit::keyboard::KeyCode;
|
||||
pub struct InputState {
|
||||
pub keys_held: HashSet<KeyCode>,
|
||||
pub mouse_locked: bool,
|
||||
|
||||
// Touch input (read from JS on WASM)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub touch_move_x: f32,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub touch_move_z: f32,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub touch_y_up: bool,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub touch_y_down: bool,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub touch_look_dx: f32,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub touch_look_dy: f32,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
@@ -11,6 +25,18 @@ impl InputState {
|
||||
Self {
|
||||
keys_held: HashSet::new(),
|
||||
mouse_locked: false,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
touch_move_x: 0.0,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
touch_move_z: 0.0,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
touch_y_up: false,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
touch_y_down: false,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
touch_look_dx: 0.0,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
touch_look_dy: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+61
@@ -79,6 +79,7 @@ impl App {
|
||||
|
||||
fn update(&mut self, dt: f32, time: f32) {
|
||||
let speed = 8.0 * dt;
|
||||
// Keyboard input
|
||||
let grid_mode = self.input.is_pressed(KeyCode::ShiftLeft)
|
||||
|| self.input.is_pressed(KeyCode::ShiftRight);
|
||||
|
||||
@@ -127,6 +128,66 @@ impl App {
|
||||
bullet.position += bullet.velocity * dt;
|
||||
}
|
||||
self.bullets.retain(|b| (time - b.spawn_time) < BULLET_LIFETIME);
|
||||
|
||||
// Touch input (WASM only)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.read_touch_state();
|
||||
|
||||
self.camera.position.x += self.input.touch_move_x * speed;
|
||||
self.camera.position.z += self.input.touch_move_z * speed;
|
||||
if self.input.touch_y_up {
|
||||
self.camera.position.y += speed;
|
||||
}
|
||||
if self.input.touch_y_down {
|
||||
self.camera.position.y -= speed;
|
||||
}
|
||||
|
||||
if self.input.touch_look_dx != 0.0 || self.input.touch_look_dy != 0.0 {
|
||||
self.camera
|
||||
.mouse_look(self.input.touch_look_dx, self.input.touch_look_dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn read_touch_state(&mut self) {
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
let Some(win) = web_sys::window() else {
|
||||
return;
|
||||
};
|
||||
let win_val: &JsValue = win.as_ref();
|
||||
let Ok(ts) = js_sys::Reflect::get(win_val, &"_touchState".into()) else {
|
||||
return;
|
||||
};
|
||||
if ts.is_undefined() || ts.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let get_f32 = |obj: &JsValue, key: &str| -> f32 {
|
||||
js_sys::Reflect::get(obj, &key.into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0) as f32
|
||||
};
|
||||
let get_bool = |obj: &JsValue, key: &str| -> bool {
|
||||
js_sys::Reflect::get(obj, &key.into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
self.input.touch_move_x = get_f32(&ts, "moveX");
|
||||
self.input.touch_move_z = get_f32(&ts, "moveZ");
|
||||
self.input.touch_y_up = get_bool(&ts, "yUp");
|
||||
self.input.touch_y_down = get_bool(&ts, "yDown");
|
||||
|
||||
// Look deltas: read and reset
|
||||
self.input.touch_look_dx = get_f32(&ts, "lookDX");
|
||||
self.input.touch_look_dy = get_f32(&ts, "lookDY");
|
||||
let _ = js_sys::Reflect::set(&ts, &"lookDX".into(), &JsValue::from(0.0));
|
||||
let _ = js_sys::Reflect::set(&ts, &"lookDY".into(), &JsValue::from(0.0));
|
||||
}
|
||||
|
||||
fn try_lock_cursor(&self, window: &Window) {
|
||||
|
||||
Reference in New Issue
Block a user