540 lines
17 KiB
HTML
540 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<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>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: #000;
|
|
touch-action: none;
|
|
}
|
|
canvas {
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
touch-action: none;
|
|
}
|
|
|
|
#help-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#help-overlay .help-box {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 12px;
|
|
padding: 32px 40px;
|
|
color: #fff;
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
font-size: 15px;
|
|
min-width: 280px;
|
|
}
|
|
|
|
#help-overlay .help-box h2 {
|
|
margin: 0 0 20px 0;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
text-align: center;
|
|
}
|
|
|
|
#help-overlay .help-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 6px 0;
|
|
}
|
|
|
|
#help-overlay .help-action {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
|
|
#help-overlay .help-keys {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
#help-overlay kbd {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
border-radius: 5px;
|
|
padding: 3px 8px;
|
|
font-family: 'SF Mono', 'Consolas', monospace;
|
|
font-size: 13px;
|
|
color: #fff;
|
|
min-width: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
#help-overlay .help-dismiss {
|
|
margin-top: 20px;
|
|
text-align: center;
|
|
color: rgba(255, 255, 255, 0.4);
|
|
font-size: 13px;
|
|
}
|
|
|
|
#reticule {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 75%;
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
display: none;
|
|
}
|
|
|
|
#reticule .reticule-h,
|
|
#reticule .reticule-v {
|
|
position: absolute;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
}
|
|
|
|
#reticule .reticule-h {
|
|
width: 20px;
|
|
height: 2px;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
#reticule .reticule-v {
|
|
width: 2px;
|
|
height: 20px;
|
|
top: 50%;
|
|
left: 50%;
|
|
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%;
|
|
left: 0;
|
|
width: 100%;
|
|
z-index: 10;
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 8px 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#scanner-separator {
|
|
position: fixed;
|
|
bottom: 25%;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 2px;
|
|
background: rgba(0, 180, 0, 0.6);
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#coord-bar .coord-inner {
|
|
background: rgba(0, 0, 0, 0.6);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 8px;
|
|
padding: 6px 20px;
|
|
font-family: 'SF Mono', 'Consolas', monospace;
|
|
font-size: 14px;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
display: flex;
|
|
gap: 24px;
|
|
}
|
|
|
|
#coord-bar .coord-label {
|
|
color: rgba(255, 255, 255, 0.45);
|
|
margin-right: 4px;
|
|
}
|
|
|
|
#coord-bar .coord-sep {
|
|
width: 1px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
align-self: stretch;
|
|
}
|
|
|
|
#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="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="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>
|
|
</div>
|
|
|
|
<div id="reticule">
|
|
<div class="reticule-h"></div>
|
|
<div class="reticule-v"></div>
|
|
</div>
|
|
|
|
<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>
|
|
<span><span class="coord-label">Y</span><span id="coord-y">-10.0</span></span>
|
|
<span><span class="coord-label">Z</span><span id="coord-z">10.0</span></span>
|
|
<span class="coord-sep"></span>
|
|
<span><span class="coord-label">Yaw</span><span id="coord-yaw">0.0</span><span class="coord-unit">°</span></span>
|
|
<span><span class="coord-label">Pitch</span><span id="coord-pitch">89.0</span><span class="coord-unit">°</span></span>
|
|
<span class="coord-sep"></span>
|
|
<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>
|