mirror of
https://github.com/sndrec/WebMonkeyBall.git
synced 2026-02-03 10:13:33 +00:00
gyro and touchscreen stuff
This commit is contained in:
31
index.html
31
index.html
@@ -55,7 +55,16 @@
|
||||
|
||||
<label id="control-mode-field" class="field hidden">
|
||||
<span>Control Mode</span>
|
||||
<select id="control-mode"></select>
|
||||
<div class="control-mode-row">
|
||||
<select id="control-mode"></select>
|
||||
<button id="gyro-recalibrate" class="ghost compact hidden" type="button">Recalibrate</button>
|
||||
<div id="gyro-helper" class="gyro-helper hidden" aria-hidden="true">
|
||||
<div class="gyro-helper-frame">
|
||||
<div id="gyro-helper-ghost" class="gyro-helper-ghost"></div>
|
||||
<div id="gyro-helper-device" class="gyro-helper-device"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<div id="smb1-fields">
|
||||
<label class="field">
|
||||
@@ -149,11 +158,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="mobile-menu-button" class="mobile-menu-button hidden" type="button">
|
||||
Level Select
|
||||
</button>
|
||||
|
||||
<script type="module">
|
||||
const field = document.getElementById('control-mode-field');
|
||||
const select = document.getElementById('control-mode');
|
||||
const hasTouch = ('ontouchstart' in window) || ((navigator.maxTouchPoints ?? 0) > 0);
|
||||
const hasGyro = typeof window.DeviceOrientationEvent !== 'undefined';
|
||||
const hasCoarsePointer = window.matchMedia?.('(pointer: coarse)')?.matches ?? false;
|
||||
const hasGyro = typeof window.DeviceOrientationEvent !== 'undefined' && (hasTouch || hasCoarsePointer);
|
||||
const gyroButton = document.getElementById('gyro-recalibrate');
|
||||
const gyroHelper = document.getElementById('gyro-helper');
|
||||
|
||||
if (!field || !select || (!hasTouch && !hasGyro)) {
|
||||
field?.classList.add('hidden');
|
||||
@@ -161,7 +177,7 @@
|
||||
field.classList.remove('hidden');
|
||||
|
||||
const options = [];
|
||||
//if (hasGyro) options.push({ value: 'gyro', label: 'Gyro' });
|
||||
if (hasGyro) options.push({ value: 'gyro', label: 'Gyro' });
|
||||
if (hasTouch) options.push({ value: 'touch', label: 'Touchscreen' });
|
||||
|
||||
select.replaceChildren(...options.map((opt) => {
|
||||
@@ -180,6 +196,14 @@
|
||||
localStorage.setItem(key, select.value);
|
||||
}
|
||||
|
||||
const syncGyroUi = () => {
|
||||
const showGyro = select.value === 'gyro';
|
||||
gyroButton?.classList.toggle('hidden', !showGyro);
|
||||
gyroHelper?.classList.toggle('hidden', !showGyro);
|
||||
};
|
||||
|
||||
syncGyroUi();
|
||||
|
||||
select.addEventListener('change', async () => {
|
||||
let next = select.value;
|
||||
|
||||
@@ -198,6 +222,7 @@
|
||||
}
|
||||
|
||||
localStorage.setItem(key, next);
|
||||
syncGyroUi();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
84
src/input.ts
84
src/input.ts
@@ -6,7 +6,8 @@ export class Input {
|
||||
|
||||
this.controlModeKey = 'smb_control_mode';
|
||||
this.hasTouch = ('ontouchstart' in window) || ((navigator.maxTouchPoints ?? 0) > 0);
|
||||
this.hasGyro = typeof window.DeviceOrientationEvent !== 'undefined';
|
||||
const hasCoarsePointer = window.matchMedia?.('(pointer: coarse)')?.matches ?? false;
|
||||
this.hasGyro = typeof window.DeviceOrientationEvent !== 'undefined' && (this.hasTouch || hasCoarsePointer);
|
||||
|
||||
this.touch = {
|
||||
pointerId: null,
|
||||
@@ -16,12 +17,22 @@ export class Input {
|
||||
value: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
this.touchAction = {
|
||||
pointerId: null,
|
||||
active: false,
|
||||
};
|
||||
|
||||
this.gyro = {
|
||||
baselineSet: false,
|
||||
baseBeta: 0,
|
||||
baseGamma: 0,
|
||||
value: { x: 0, y: 0 },
|
||||
};
|
||||
this.gyroRaw = {
|
||||
beta: 0,
|
||||
gamma: 0,
|
||||
hasSample: false,
|
||||
};
|
||||
|
||||
this.touchRoot = document.getElementById('touch-controls');
|
||||
this.joystickEl = this.touchRoot?.querySelector?.('.joystick') ?? null;
|
||||
@@ -98,12 +109,42 @@ export class Input {
|
||||
if (event.pointerId === this.touch.pointerId) {
|
||||
this.endTouch();
|
||||
}
|
||||
if (event.pointerId === this.touchAction.pointerId) {
|
||||
this.endTouchAction();
|
||||
}
|
||||
},
|
||||
|
||||
pointercancel: (event) => {
|
||||
if (event.pointerId === this.touch.pointerId) {
|
||||
this.endTouch();
|
||||
}
|
||||
if (event.pointerId === this.touchAction.pointerId) {
|
||||
this.endTouchAction();
|
||||
}
|
||||
},
|
||||
|
||||
pointerdownAction: (event) => {
|
||||
if (!this.hasTouch) {
|
||||
return;
|
||||
}
|
||||
if (event.pointerType !== 'touch') {
|
||||
return;
|
||||
}
|
||||
if (this.isOverlayVisible()) {
|
||||
return;
|
||||
}
|
||||
if (event.target instanceof HTMLElement) {
|
||||
const interactive = event.target.closest('button, input, select, textarea, a, label');
|
||||
if (interactive) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.touchAction.pointerId !== null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.touchAction.pointerId = event.pointerId;
|
||||
this.touchAction.active = true;
|
||||
},
|
||||
|
||||
deviceorientation: (event) => {
|
||||
@@ -116,6 +157,9 @@ export class Input {
|
||||
|
||||
const beta = typeof event.beta === 'number' ? event.beta : 0;
|
||||
const gamma = typeof event.gamma === 'number' ? event.gamma : 0;
|
||||
this.gyroRaw.beta = beta;
|
||||
this.gyroRaw.gamma = gamma;
|
||||
this.gyroRaw.hasSample = true;
|
||||
|
||||
if (!this.gyro.baselineSet) {
|
||||
this.gyro.baselineSet = true;
|
||||
@@ -150,6 +194,9 @@ export class Input {
|
||||
window.addEventListener('pointerup', this.handlers.pointerup);
|
||||
window.addEventListener('pointercancel', this.handlers.pointercancel);
|
||||
}
|
||||
if (this.hasTouch) {
|
||||
window.addEventListener('pointerdown', this.handlers.pointerdownAction, { passive: false });
|
||||
}
|
||||
|
||||
if (this.hasGyro) {
|
||||
window.addEventListener('deviceorientation', this.handlers.deviceorientation);
|
||||
@@ -169,6 +216,9 @@ export class Input {
|
||||
window.removeEventListener('pointerup', this.handlers.pointerup);
|
||||
window.removeEventListener('pointercancel', this.handlers.pointercancel);
|
||||
}
|
||||
if (this.hasTouch) {
|
||||
window.removeEventListener('pointerdown', this.handlers.pointerdownAction);
|
||||
}
|
||||
|
||||
if (this.hasGyro) {
|
||||
window.removeEventListener('deviceorientation', this.handlers.deviceorientation);
|
||||
@@ -254,10 +304,39 @@ export class Input {
|
||||
this.hideJoystick();
|
||||
}
|
||||
|
||||
endTouchAction() {
|
||||
this.touchAction.active = false;
|
||||
this.touchAction.pointerId = null;
|
||||
}
|
||||
|
||||
isDown(code) {
|
||||
return this.down.has(code);
|
||||
}
|
||||
|
||||
recalibrateGyro() {
|
||||
if (!this.hasGyro) {
|
||||
return;
|
||||
}
|
||||
if (this.gyroRaw.hasSample) {
|
||||
this.gyro.baseBeta = this.gyroRaw.beta;
|
||||
this.gyro.baseGamma = this.gyroRaw.gamma;
|
||||
this.gyro.baselineSet = true;
|
||||
} else {
|
||||
this.gyro.baselineSet = false;
|
||||
}
|
||||
}
|
||||
|
||||
getGyroSample() {
|
||||
return {
|
||||
beta: this.gyroRaw.beta,
|
||||
gamma: this.gyroRaw.gamma,
|
||||
baseBeta: this.gyro.baseBeta,
|
||||
baseGamma: this.gyro.baseGamma,
|
||||
baselineSet: this.gyro.baselineSet,
|
||||
hasSample: this.gyroRaw.hasSample,
|
||||
};
|
||||
}
|
||||
|
||||
wasPressed(code) {
|
||||
if (this.pressed.has(code)) {
|
||||
this.pressed.delete(code);
|
||||
@@ -297,6 +376,9 @@ export class Input {
|
||||
if (this.isDown('Space') || this.isDown('Enter') || this.isDown('KeyZ')) {
|
||||
return true;
|
||||
}
|
||||
if (this.touchAction.active) {
|
||||
return true;
|
||||
}
|
||||
const pad = this.getActiveGamepad();
|
||||
if (!pad?.buttons?.length) {
|
||||
return false;
|
||||
|
||||
66
src/main.ts
66
src/main.ts
@@ -34,6 +34,16 @@ import type { StageData } from './noclip/SuperMonkeyBall/World.js';
|
||||
import { convertSmb2StageDef, getMb2wsStageInfo, getSmb2StageInfo } from './smb2_render.js';
|
||||
import { HudRenderer } from './hud.js';
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const STAGE_BASE_PATH = STAGE_BASE_PATHS[GAME_SOURCES.SMB1];
|
||||
const NAOMI_STAGE_IDS = new Set([
|
||||
10, 19, 20, 30, 49, 50, 60, 70, 80, 92, 96, 97, 98, 99, 100, 114, 115, 116, 117, 118, 119, 120,
|
||||
@@ -47,6 +57,11 @@ const canvas = document.getElementById('game') as HTMLCanvasElement;
|
||||
const hudCanvas = document.getElementById('hud-canvas') as HTMLCanvasElement;
|
||||
const overlay = document.getElementById('overlay') as HTMLElement;
|
||||
const stageFade = document.getElementById('stage-fade') as HTMLElement;
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button') as HTMLButtonElement | null;
|
||||
const controlModeSelect = document.getElementById('control-mode') as HTMLSelectElement | null;
|
||||
const gyroRecalibrateButton = document.getElementById('gyro-recalibrate') as HTMLButtonElement | null;
|
||||
const gyroHelper = document.getElementById('gyro-helper') as HTMLElement | null;
|
||||
const gyroHelperFrame = gyroHelper?.querySelector('.gyro-helper-frame') as HTMLElement | null;
|
||||
const startButton = document.getElementById('start') as HTMLButtonElement;
|
||||
const resumeButton = document.getElementById('resume') as HTMLButtonElement;
|
||||
const difficultySelect = document.getElementById('difficulty') as HTMLSelectElement;
|
||||
@@ -69,10 +84,22 @@ const announcerVolumeValue = document.getElementById('announcer-volume-value') a
|
||||
|
||||
const hudStatus = document.getElementById('hud-status') as HTMLElement | null;
|
||||
|
||||
const setOverlayVisible = (visible: boolean) => {
|
||||
const hasTouch = ('ontouchstart' in window) || ((navigator.maxTouchPoints ?? 0) > 0);
|
||||
|
||||
function updateMobileMenuButtonVisibility() {
|
||||
if (!mobileMenuButton) {
|
||||
return;
|
||||
}
|
||||
const shouldShow = hasTouch && overlay.classList.contains('hidden') && running;
|
||||
mobileMenuButton.classList.toggle('hidden', !shouldShow);
|
||||
}
|
||||
|
||||
function setOverlayVisible(visible: boolean) {
|
||||
overlay.classList.toggle('hidden', !visible);
|
||||
canvas.style.pointerEvents = visible ? 'none' : 'auto';
|
||||
};
|
||||
document.body.classList.toggle('gameplay-active', !visible);
|
||||
updateMobileMenuButtonVisibility();
|
||||
}
|
||||
|
||||
const STAGE_FADE_MS = 333;
|
||||
|
||||
@@ -295,9 +322,11 @@ const game = new Game({
|
||||
resumeButton.disabled = false;
|
||||
},
|
||||
onPaused: () => {
|
||||
paused = true;
|
||||
setOverlayVisible(true);
|
||||
},
|
||||
onResumed: () => {
|
||||
paused = false;
|
||||
setOverlayVisible(false);
|
||||
},
|
||||
onStageLoaded: (stageId) => {
|
||||
@@ -437,6 +466,7 @@ async function handleStageLoaded(stageId: number) {
|
||||
paused = false;
|
||||
renderReady = true;
|
||||
lastTime = performance.now();
|
||||
updateMobileMenuButtonVisibility();
|
||||
maybeStartSmb2LikeStageFade();
|
||||
return;
|
||||
}
|
||||
@@ -462,6 +492,7 @@ async function handleStageLoaded(stageId: number) {
|
||||
paused = false;
|
||||
renderReady = true;
|
||||
lastTime = performance.now();
|
||||
updateMobileMenuButtonVisibility();
|
||||
maybeStartSmb2LikeStageFade();
|
||||
}
|
||||
|
||||
@@ -648,6 +679,7 @@ function renderFrame(now: number) {
|
||||
resizeCanvasToDisplaySize(canvas);
|
||||
resizeCanvasToDisplaySize(hudCanvas);
|
||||
hudRenderer.resize(hudCanvas.width, hudCanvas.height);
|
||||
updateGyroHelper();
|
||||
|
||||
if (game.loadingStage) {
|
||||
const hudDelta = now - lastHudTime;
|
||||
@@ -699,6 +731,27 @@ function renderFrame(now: number) {
|
||||
hudRenderer.render(game, dtSeconds);
|
||||
}
|
||||
|
||||
function updateGyroHelper() {
|
||||
if (!controlModeSelect || !gyroHelper || !gyroHelperFrame) {
|
||||
return;
|
||||
}
|
||||
const showGyro = controlModeSelect.value === 'gyro';
|
||||
gyroHelper.classList.toggle('hidden', !showGyro);
|
||||
if (!showGyro) {
|
||||
return;
|
||||
}
|
||||
const sample = game.input?.getGyroSample?.();
|
||||
if (!sample || !sample.hasSample) {
|
||||
gyroHelperFrame.style.opacity = '0.5';
|
||||
return;
|
||||
}
|
||||
const x = clamp(-sample.beta, -30, 30);
|
||||
const y = clamp(sample.gamma, -30, 30);
|
||||
gyroHelperFrame.style.opacity = '1';
|
||||
gyroHelperFrame.style.setProperty('--gyro-x', `${x}deg`);
|
||||
gyroHelperFrame.style.setProperty('--gyro-y', `${y}deg`);
|
||||
}
|
||||
|
||||
setOverlayVisible(true);
|
||||
startButton.disabled = false;
|
||||
|
||||
@@ -759,6 +812,15 @@ resumeButton.addEventListener('click', () => {
|
||||
game.resume();
|
||||
});
|
||||
|
||||
gyroRecalibrateButton?.addEventListener('click', () => {
|
||||
game.input?.recalibrateGyro?.();
|
||||
});
|
||||
|
||||
mobileMenuButton?.addEventListener('click', () => {
|
||||
paused = true;
|
||||
game.pause();
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
paused = true;
|
||||
|
||||
99
style.css
99
style.css
@@ -21,12 +21,20 @@ body {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
body.gameplay-active {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
#game {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
#hud-canvas {
|
||||
@@ -37,6 +45,8 @@ body {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 6;
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.touch-controls {
|
||||
@@ -45,6 +55,7 @@ body {
|
||||
z-index: 7;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.touch-controls.active {
|
||||
@@ -107,6 +118,7 @@ body {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 10;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.overlay.hidden {
|
||||
@@ -118,12 +130,15 @@ body {
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: min(520px, 92vw);
|
||||
width: min(560px, 94vw);
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.panel h1 {
|
||||
@@ -145,6 +160,65 @@ body {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.control-mode-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.control-mode-row select {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
|
||||
.control-mode-row .compact {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gyro-helper {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gyro-helper-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(8, 8, 12, 0.6);
|
||||
box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.45);
|
||||
perspective: 220px;
|
||||
--gyro-x: 0deg;
|
||||
--gyro-y: 0deg;
|
||||
}
|
||||
|
||||
.gyro-helper-ghost,
|
||||
.gyro-helper-device {
|
||||
position: absolute;
|
||||
inset: 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.gyro-helper-ghost {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
transform: rotateX(60deg) rotateZ(45deg);
|
||||
}
|
||||
|
||||
.gyro-helper-device {
|
||||
background: rgba(255, 159, 28, 0.2);
|
||||
border-color: rgba(255, 159, 28, 0.65);
|
||||
transform: rotateX(calc(60deg + var(--gyro-x))) rotateZ(45deg) rotateY(var(--gyro-y));
|
||||
transition: transform 80ms linear;
|
||||
}
|
||||
|
||||
.checkbox-field {
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -320,4 +394,25 @@ button:disabled {
|
||||
.credits-panel span {
|
||||
color: var(--muted);
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-button {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(420px, 92vw);
|
||||
height: 40px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
background: rgba(8, 8, 12, 0.78);
|
||||
color: var(--fg);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user