Replace dpad with virtual joystick

main
darcy 4 weeks ago
parent 775cb3ecd0
commit 200d0e6e32

@ -57,11 +57,8 @@
<footer class="controls"> <footer class="controls">
<div class="touch-controls" aria-label="触摸操作"> <div class="touch-controls" aria-label="触摸操作">
<div class="dpad" aria-label="方向控制"> <div id="moveStick" class="move-stick" aria-label="方向摇杆" role="application">
<button class="touch-button dpad-up" type="button" data-control="up" aria-label="向上"></button> <div id="moveKnob" class="move-knob" aria-hidden="true"></div>
<button class="touch-button dpad-left" type="button" data-control="left" aria-label="向左"></button>
<button class="touch-button dpad-down" type="button" data-control="down" aria-label="向下"></button>
<button class="touch-button dpad-right" type="button" data-control="right" aria-label="向右"></button>
</div> </div>
<button id="fireAction" class="fire-action" type="button" aria-label="开火">开火</button> <button id="fireAction" class="fire-action" type="button" aria-label="开火">开火</button>
</div> </div>

@ -69,7 +69,8 @@
const stopAction = document.querySelector("#stopAction"); const stopAction = document.querySelector("#stopAction");
const endAction = document.querySelector("#endAction"); const endAction = document.querySelector("#endAction");
const fireAction = document.querySelector("#fireAction"); const fireAction = document.querySelector("#fireAction");
const touchButtons = [...document.querySelectorAll("[data-control]")]; const moveStick = document.querySelector("#moveStick");
const moveKnob = document.querySelector("#moveKnob");
const WIDTH = canvas.width; const WIDTH = canvas.width;
const HEIGHT = canvas.height; const HEIGHT = canvas.height;
@ -619,21 +620,21 @@
if (isPlayer) { if (isPlayer) {
if (health > 66) { if (health > 66) {
glowColor = "rgba(132, 238, 212, 0.75)"; glowColor = "rgba(132, 238, 212, 0.55)";
glowBlur = 12; glowBlur = 7;
outerColor = "#e8fff7"; outerColor = "#e8fff7";
innerColor = "#102822"; innerColor = "#102822";
coreColor = entity.color; coreColor = entity.color;
} else if (health > 33) { } else if (health > 33) {
glowColor = "rgba(240, 182, 70, 0.45)"; glowColor = "rgba(240, 182, 70, 0.45)";
glowBlur = 8; glowBlur = 6;
outerColor = "#c8c0a0"; outerColor = "#c8c0a0";
innerColor = "#1e2818"; innerColor = "#1e2818";
coreColor = "#9a9850"; coreColor = "#9a9850";
} else { } else {
const pulse = 0.5 + 0.5 * Math.sin(Date.now() / 180); const pulse = 0.5 + 0.5 * Math.sin(Date.now() / 180);
glowColor = `rgba(212, 93, 69, ${0.5 + 0.4 * pulse})`; glowColor = `rgba(212, 93, 69, ${0.42 + 0.32 * pulse})`;
glowBlur = 10 + 6 * pulse; glowBlur = 7 + 4 * pulse;
outerColor = "#3a2020"; outerColor = "#3a2020";
innerColor = "#1a0c0c"; innerColor = "#1a0c0c";
coreColor = "#6b2828"; coreColor = "#6b2828";
@ -766,38 +767,63 @@
keys.delete(event.key.toLowerCase()); keys.delete(event.key.toLowerCase());
}); });
function bindHoldButton(button, key) { const stickKeys = ["arrowup", "arrowdown", "arrowleft", "arrowright"];
const press = (event) => { let activeStickKey = null;
event.preventDefault();
button.setPointerCapture?.(event.pointerId); function clearStickDirection() {
keys.add(key); if (activeStickKey) keys.delete(activeStickKey);
canvas.focus({ preventScroll: true }); activeStickKey = null;
}; if (moveKnob) moveKnob.style.transform = "translate(-50%, -50%)";
const release = (event) => { }
event.preventDefault();
keys.delete(key); function setStickDirection(key) {
try { if (activeStickKey === key) return;
button.releasePointerCapture?.(event.pointerId); if (activeStickKey) keys.delete(activeStickKey);
} catch { activeStickKey = key;
// Some browsers release capture automatically on cancel/leave. if (key) keys.add(key);
} }
};
button.addEventListener("pointerdown", press); function updateStick(event) {
button.addEventListener("pointerup", release); if (!moveStick) return;
button.addEventListener("pointercancel", release); event.preventDefault();
button.addEventListener("pointerleave", release); const rect = moveStick.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = event.clientX - centerX;
const dy = event.clientY - centerY;
const distance = Math.hypot(dx, dy);
const deadZone = rect.width * 0.12;
if (distance < deadZone) {
clearStickDirection();
return;
}
const key = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? "arrowright" : "arrowleft") : dy > 0 ? "arrowdown" : "arrowup";
setStickDirection(key);
if (moveKnob) {
const max = rect.width * 0.24;
const scale = Math.min(max, distance) / distance;
moveKnob.style.transform = `translate(calc(-50% + ${dx * scale}px), calc(-50% + ${dy * scale}px))`;
}
canvas.focus({ preventScroll: true });
} }
const touchKeyMap = { if (moveStick) {
up: "arrowup", moveStick.addEventListener("pointerdown", (event) => {
down: "arrowdown", moveStick.setPointerCapture?.(event.pointerId);
left: "arrowleft", updateStick(event);
right: "arrowright", });
}; moveStick.addEventListener("pointermove", updateStick);
for (const type of ["pointerup", "pointercancel", "pointerleave"]) {
for (const button of touchButtons) { moveStick.addEventListener(type, (event) => {
const key = touchKeyMap[button.dataset.control]; event.preventDefault();
if (key) bindHoldButton(button, key); clearStickDirection();
try {
moveStick.releasePointerCapture?.(event.pointerId);
} catch {
// Some browsers release capture automatically.
}
});
}
} }
primaryAction.addEventListener("click", startOrResume); primaryAction.addEventListener("click", startOrResume);

@ -299,50 +299,62 @@ canvas {
align-items: center; align-items: center;
} }
.dpad { .move-stick {
position: relative;
width: min(190px, 48vw); width: min(190px, 48vw);
aspect-ratio: 1; aspect-ratio: 1;
display: grid; border: 1px solid var(--line-bright);
grid-template-columns: repeat(3, minmax(0, 1fr)); border-radius: 50%;
grid-template-rows: repeat(3, minmax(0, 1fr)); background:
gap: 7px; radial-gradient(circle at 50% 50%, rgba(240, 182, 70, 0.1) 0 24%, transparent 25%),
touch-action: none; radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.07), transparent 58%),
user-select: none; var(--panel);
} box-shadow: inset 0 0 0 8px rgba(16, 21, 13, 0.72), 0 12px 28px var(--shadow);
.touch-button,
.fire-action {
min-height: 54px;
padding: 0;
display: grid;
place-items: center;
touch-action: none; touch-action: none;
user-select: none; user-select: none;
-webkit-user-select: none;
} }
.touch-button { .move-stick::before,
font-size: 1.25rem; .move-stick::after {
content: "";
position: absolute;
inset: 50% auto auto 50%;
background: rgba(198, 216, 123, 0.45);
transform: translate(-50%, -50%);
pointer-events: none;
} }
.dpad-up { .move-stick::before {
grid-column: 2; width: 70%;
grid-row: 1; height: 2px;
} }
.dpad-left { .move-stick::after {
grid-column: 1; width: 2px;
grid-row: 2; height: 70%;
} }
.dpad-down { .move-knob {
grid-column: 2; position: absolute;
grid-row: 3; left: 50%;
top: 50%;
width: 42%;
aspect-ratio: 1;
border: 1px solid #d8e58c;
border-radius: 50%;
background: linear-gradient(180deg, #adc764, #718536);
box-shadow: 0 8px 18px var(--shadow);
transform: translate(-50%, -50%);
pointer-events: none;
} }
.dpad-right { .fire-action {
grid-column: 3; min-height: 54px;
grid-row: 2; padding: 0;
display: grid;
place-items: center;
touch-action: none;
-webkit-user-select: none;
} }
.fire-action { .fire-action {
@ -450,12 +462,10 @@ kbd {
row-gap: 10px; row-gap: 10px;
} }
.dpad { .move-stick {
width: 100%; width: 100%;
gap: 6px;
} }
.touch-button,
.fire-action { .fire-action {
min-height: 50px; min-height: 50px;
} }

Loading…
Cancel
Save