Replace mobile buttons with drag joystick
This commit is contained in:
parent
04103573d0
commit
47c251208c
186
index.html
186
index.html
@ -20,16 +20,69 @@
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
#drag-stick {
|
||||
position: absolute;
|
||||
left: max(18px, env(safe-area-inset-left));
|
||||
bottom: max(22px, env(safe-area-inset-bottom));
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
border-radius: 50%;
|
||||
background: rgba(7, 16, 24, 0.42);
|
||||
box-shadow: inset 0 0 0 1px rgba(140, 211, 255, 0.08);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#drag-stick::before,
|
||||
#drag-stick::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: rgba(232, 242, 247, 0.18);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#drag-stick::before {
|
||||
left: 50%;
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
#drag-stick::after {
|
||||
top: 50%;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
#drag-thumb {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
margin: -29px 0 0 -29px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
background: rgba(140, 211, 255, 0.28);
|
||||
box-shadow: 0 3px 18px rgba(0, 0, 0, 0.28);
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.mobile-control {
|
||||
position: absolute;
|
||||
min-width: 62px;
|
||||
height: 62px;
|
||||
min-width: 72px;
|
||||
height: 64px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border-radius: 18px;
|
||||
background: rgba(7, 16, 24, 0.68);
|
||||
color: #e8f2f7;
|
||||
font: 700 18px/1 system-ui, sans-serif;
|
||||
font: 700 13px/1 system-ui, sans-serif;
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
pointer-events: auto;
|
||||
@ -42,13 +95,26 @@
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.mobile-control.left { left: max(14px, env(safe-area-inset-left)); bottom: max(18px, env(safe-area-inset-bottom)); }
|
||||
.mobile-control.right { left: max(88px, calc(env(safe-area-inset-left) + 74px)); bottom: max(18px, env(safe-area-inset-bottom)); }
|
||||
.mobile-control.throttle { right: max(14px, env(safe-area-inset-right)); bottom: max(92px, calc(env(safe-area-inset-bottom) + 74px)); }
|
||||
.mobile-control.reverse { right: max(14px, env(safe-area-inset-right)); bottom: max(18px, env(safe-area-inset-bottom)); }
|
||||
.mobile-control.port { left: max(14px, env(safe-area-inset-left)); bottom: max(100px, calc(env(safe-area-inset-bottom) + 82px)); font-size: 13px; }
|
||||
.mobile-control.starboard { right: max(88px, calc(env(safe-area-inset-right) + 74px)); bottom: max(18px, env(safe-area-inset-bottom)); font-size: 13px; }
|
||||
.mobile-control.restart { top: max(14px, env(safe-area-inset-top)); right: max(14px, env(safe-area-inset-right)); width: 48px; min-width: 48px; height: 48px; border-radius: 50%; }
|
||||
.mobile-control.port {
|
||||
right: max(18px, env(safe-area-inset-right));
|
||||
bottom: max(100px, calc(env(safe-area-inset-bottom) + 82px));
|
||||
}
|
||||
|
||||
.mobile-control.starboard {
|
||||
right: max(18px, env(safe-area-inset-right));
|
||||
bottom: max(26px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.mobile-control.restart {
|
||||
top: max(14px, env(safe-area-inset-top));
|
||||
right: max(14px, env(safe-area-inset-right));
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
#mobile-controls { display: none; }
|
||||
@ -61,53 +127,125 @@
|
||||
</div>
|
||||
|
||||
<div id="mobile-controls" aria-label="Touch controls">
|
||||
<button class="mobile-control left" data-code="ArrowLeft" data-key="ArrowLeft" data-keycode="37" aria-label="Steer left">←</button>
|
||||
<button class="mobile-control right" data-code="ArrowRight" data-key="ArrowRight" data-keycode="39" aria-label="Steer right">→</button>
|
||||
<button class="mobile-control throttle" data-code="ArrowUp" data-key="ArrowUp" data-keycode="38" aria-label="Throttle">▲</button>
|
||||
<button class="mobile-control reverse" data-code="ArrowDown" data-key="ArrowDown" data-keycode="40" aria-label="Reverse">▼</button>
|
||||
<div id="drag-stick" aria-label="Drag to steer and throttle">
|
||||
<div id="drag-thumb"></div>
|
||||
</div>
|
||||
<button class="mobile-control port" data-code="KeyQ" data-key="q" data-keycode="81" aria-label="Fire port broadside">PORT</button>
|
||||
<button class="mobile-control starboard" data-code="KeyE" data-key="e" data-keycode="69" aria-label="Fire starboard broadside">STARBOARD</button>
|
||||
<button class="mobile-control restart" data-code="KeyR" data-key="r" data-keycode="82" aria-label="Restart">↻</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/game-v3.js"></script>
|
||||
<script type="module" src="/src/game-v4.js"></script>
|
||||
<script type="module">
|
||||
const dispatchKey = (type, button) => {
|
||||
const keyCode = Number(button.dataset.keycode);
|
||||
const keyMap = {
|
||||
ArrowLeft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
|
||||
ArrowUp: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
|
||||
ArrowRight: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
|
||||
ArrowDown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
|
||||
};
|
||||
|
||||
const dispatchKey = (type, key, code, keyCode) => {
|
||||
const event = new KeyboardEvent(type, {
|
||||
key: button.dataset.key,
|
||||
code: button.dataset.code,
|
||||
key,
|
||||
code,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(event, 'keyCode', { get: () => keyCode });
|
||||
Object.defineProperty(event, 'which', { get: () => keyCode });
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const held = new Set();
|
||||
const setHeld = (code, isDown) => {
|
||||
if (isDown === held.has(code)) return;
|
||||
const meta = keyMap[code];
|
||||
if (!meta) return;
|
||||
dispatchKey(isDown ? 'keydown' : 'keyup', meta.key, meta.code, meta.keyCode);
|
||||
if (isDown) held.add(code);
|
||||
else held.delete(code);
|
||||
};
|
||||
|
||||
const releaseStick = () => {
|
||||
for (const code of [...held]) setHeld(code, false);
|
||||
document.querySelector('#drag-thumb').style.transform = 'translate(0px, 0px)';
|
||||
};
|
||||
|
||||
const stick = document.querySelector('#drag-stick');
|
||||
const thumb = document.querySelector('#drag-thumb');
|
||||
const radius = 45;
|
||||
const deadZone = 12;
|
||||
let stickPointer = null;
|
||||
|
||||
const updateStick = (event) => {
|
||||
const rect = stick.getBoundingClientRect();
|
||||
let dx = event.clientX - (rect.left + rect.width / 2);
|
||||
let dy = event.clientY - (rect.top + rect.height / 2);
|
||||
const length = Math.hypot(dx, dy);
|
||||
if (length > radius) {
|
||||
dx = dx / length * radius;
|
||||
dy = dy / length * radius;
|
||||
}
|
||||
|
||||
thumb.style.transform = `translate(${dx}px, ${dy}px)`;
|
||||
setHeld('ArrowLeft', dx < -deadZone);
|
||||
setHeld('ArrowRight', dx > deadZone);
|
||||
setHeld('ArrowUp', dy < -deadZone);
|
||||
setHeld('ArrowDown', dy > deadZone);
|
||||
};
|
||||
|
||||
stick.addEventListener('pointerdown', (event) => {
|
||||
event.preventDefault();
|
||||
stickPointer = event.pointerId;
|
||||
stick.setPointerCapture?.(event.pointerId);
|
||||
updateStick(event);
|
||||
});
|
||||
|
||||
stick.addEventListener('pointermove', (event) => {
|
||||
if (event.pointerId !== stickPointer) return;
|
||||
event.preventDefault();
|
||||
updateStick(event);
|
||||
});
|
||||
|
||||
const endStick = (event) => {
|
||||
if (stickPointer !== null && event.pointerId !== undefined && event.pointerId !== stickPointer) return;
|
||||
event.preventDefault?.();
|
||||
stickPointer = null;
|
||||
releaseStick();
|
||||
};
|
||||
|
||||
stick.addEventListener('pointerup', endStick);
|
||||
stick.addEventListener('pointercancel', endStick);
|
||||
stick.addEventListener('lostpointercapture', endStick);
|
||||
stick.addEventListener('contextmenu', (event) => event.preventDefault());
|
||||
|
||||
for (const button of document.querySelectorAll('.mobile-control')) {
|
||||
const fire = (type) => {
|
||||
dispatchKey(type, button.dataset.key, button.dataset.code, Number(button.dataset.keycode));
|
||||
};
|
||||
|
||||
const release = (event) => {
|
||||
event.preventDefault();
|
||||
dispatchKey('keyup', button);
|
||||
fire('keyup');
|
||||
button.classList.remove('active');
|
||||
};
|
||||
|
||||
button.addEventListener('pointerdown', (event) => {
|
||||
event.preventDefault();
|
||||
button.setPointerCapture?.(event.pointerId);
|
||||
dispatchKey('keydown', button);
|
||||
fire('keydown');
|
||||
button.classList.add('active');
|
||||
});
|
||||
|
||||
button.addEventListener('pointerup', release);
|
||||
button.addEventListener('pointercancel', release);
|
||||
button.addEventListener('lostpointercapture', () => {
|
||||
dispatchKey('keyup', button);
|
||||
fire('keyup');
|
||||
button.classList.remove('active');
|
||||
});
|
||||
button.addEventListener('contextmenu', (event) => event.preventDefault());
|
||||
}
|
||||
|
||||
window.addEventListener('blur', releaseStick);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user