Files

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 &middot; 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">&#9650;</button>
<button id="y-down-btn" class="y-btn">&#9660;</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">&deg;</span></span>
<span><span class="coord-label">Pitch</span><span id="coord-pitch">89.0</span><span class="coord-unit">&deg;</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>