gyro A press input, other stuff

This commit is contained in:
Brandon Johnson
2026-01-26 12:40:30 -05:00
parent 0aa0ba6c09
commit 1f9003260a
5 changed files with 114 additions and 5 deletions

View File

@@ -201,6 +201,10 @@
Level Select
</button>
<button id="fullscreen-button" class="ghost compact fullscreen-button hidden" type="button">
Fullscreen
</button>
<div id="gamepad-calibration" class="modal hidden" role="dialog" aria-modal="true" aria-hidden="true">
<div class="modal-card">
<h2>Stick Calibration</h2>

View File

@@ -1734,6 +1734,7 @@ export class Game {
&& !timeoverActive;
const switchesEnabled = this.goalTimerFrames <= 0 && !ringoutActive && !timeoverActive;
const isBonusStage = this.isBonusStageActive();
this.input?.setGyroTapMode?.(inputEnabled ? 'recalibrate' : 'action');
const fastForwardIntro = this.stageAttempts === 1
&& this.introTimerFrames > 120
&& this.input?.isPrimaryActionDown?.();

View File

@@ -39,6 +39,7 @@ export class Input {
this.inputFalloff = 1;
this.touchPreview = false;
this.padGate = loadPadGate() ?? DEFAULT_STICK_GATE.map((point) => [point[0], point[1]]);
this.gyroTapMode = 'recalibrate';
this.touchRoot = document.getElementById('touch-controls');
this.joystickEl = this.touchRoot?.querySelector?.('.joystick') ?? null;
@@ -157,7 +158,7 @@ export class Input {
if (this.isOverlayVisible()) {
return;
}
if (this.getControlMode() === 'gyro') {
if (this.getControlMode() === 'gyro' && this.gyroTapMode === 'recalibrate') {
event.preventDefault();
this.recalibrateGyro();
return;
@@ -184,8 +185,9 @@ export class Input {
return;
}
const beta = typeof event.beta === 'number' ? event.beta : 0;
const gamma = typeof event.gamma === 'number' ? event.gamma : 0;
const rawBeta = typeof event.beta === 'number' ? event.beta : 0;
const rawGamma = typeof event.gamma === 'number' ? event.gamma : 0;
const { beta, gamma } = adjustGyroForOrientation(rawBeta, rawGamma);
this.gyroRaw.beta = beta;
this.gyroRaw.gamma = gamma;
this.gyroRaw.hasSample = true;
@@ -329,6 +331,9 @@ export class Input {
if (!this.joystickHandleEl) {
return;
}
if (this.joystickEl?.classList.contains('hidden') && !this.isOverlayVisible()) {
this.joystickEl.classList.remove('hidden');
}
const scale = this.joystickScale || 1;
const adjX = dx / scale;
const adjY = dy / scale;
@@ -400,6 +405,10 @@ export class Input {
this.inputFalloff = clamp(value, 1, 2);
}
setGyroTapMode(mode) {
this.gyroTapMode = mode === 'action' ? 'action' : 'recalibrate';
}
setPadGate(points) {
if (!Array.isArray(points) || points.length !== 8) {
return;
@@ -730,3 +739,26 @@ function applyInputFalloff(stick, power) {
const scale = eased / maxAxis;
return { x: stick.x * scale, y: stick.y * scale };
}
function adjustGyroForOrientation(beta, gamma) {
const angle = getScreenOrientationAngle();
switch (angle) {
case 90:
return { beta: -gamma, gamma: beta };
case 180:
return { beta: -beta, gamma: -gamma };
case 270:
return { beta: gamma, gamma: -beta };
default:
return { beta, gamma };
}
}
function getScreenOrientationAngle() {
const orientation = window.screen?.orientation?.angle;
if (typeof orientation === 'number') {
return ((orientation % 360) + 360) % 360;
}
const legacy = typeof window.orientation === 'number' ? window.orientation : 0;
return ((legacy % 360) + 360) % 360;
}

View File

@@ -67,8 +67,10 @@ function isNaomiStage(stageId: number): boolean {
const canvas = document.getElementById('game') as HTMLCanvasElement;
const hudCanvas = document.getElementById('hud-canvas') as HTMLCanvasElement;
const overlay = document.getElementById('overlay') as HTMLElement;
const overlayPanel = document.querySelector('.panel') as HTMLElement | null;
const stageFade = document.getElementById('stage-fade') as HTMLElement;
const mobileMenuButton = document.getElementById('mobile-menu-button') as HTMLButtonElement | null;
const fullscreenButton = document.getElementById('fullscreen-button') as HTMLButtonElement | null;
const controlModeField = document.getElementById('control-mode-field') as HTMLElement | null;
const controlModeSelect = document.getElementById('control-mode') as HTMLSelectElement | null;
const gyroRecalibrateButton = document.getElementById('gyro-recalibrate') as HTMLButtonElement | null;
@@ -127,11 +129,29 @@ function updateMobileMenuButtonVisibility() {
mobileMenuButton.classList.toggle('hidden', !shouldShow);
}
function updateFullscreenButtonVisibility() {
if (!fullscreenButton) {
return;
}
const root = document.documentElement as HTMLElement & {
webkitRequestFullscreen?: () => Promise<void> | void;
};
const supportsFullscreen = typeof root.requestFullscreen === 'function' || typeof root.webkitRequestFullscreen === 'function';
const isFullscreen = !!(document.fullscreenElement || (document as typeof document & { webkitFullscreenElement?: Element | null }).webkitFullscreenElement);
const shouldShow = hasTouch && supportsFullscreen;
fullscreenButton.classList.toggle('hidden', !shouldShow);
if (!shouldShow) {
return;
}
fullscreenButton.textContent = supportsFullscreen && isFullscreen ? 'Exit Fullscreen' : 'Fullscreen';
}
function setOverlayVisible(visible: boolean) {
overlay.classList.toggle('hidden', !visible);
canvas.style.pointerEvents = visible ? 'none' : 'auto';
document.body.classList.toggle('gameplay-active', !visible);
updateMobileMenuButtonVisibility();
updateFullscreenButtonVisibility();
syncTouchPreviewVisibility();
}
@@ -934,10 +954,18 @@ function maybeUpdateControlModeSettings(now: number) {
function syncTouchPreviewVisibility() {
const overlayVisible = !overlay.classList.contains('hidden');
const mode = controlModeSelect?.value;
const shouldPreview = overlayVisible && mode === 'touch';
const shouldPreview = overlayVisible && mode === 'touch' && !isOverlayPanelNearBottom();
game.input?.setTouchPreview?.(shouldPreview);
}
function isOverlayPanelNearBottom() {
if (!overlayPanel) {
return false;
}
const buffer = 24;
return overlayPanel.scrollTop + overlayPanel.clientHeight >= overlayPanel.scrollHeight - buffer;
}
function renderFrame(now: number) {
requestAnimationFrame(renderFrame);
@@ -1118,6 +1146,7 @@ bindRangeControl(
updateControlModeSettingsVisibility();
updateFalloffCurve(game.input?.inputFalloff ?? 1);
syncTouchPreviewVisibility();
updateFullscreenButtonVisibility();
smb2ModeSelect?.addEventListener('change', () => {
updateSmb2ModeFields();
@@ -1140,6 +1169,40 @@ controlModeSelect?.addEventListener('change', () => {
syncTouchPreviewVisibility();
});
fullscreenButton?.addEventListener('click', async () => {
const root = document.documentElement as HTMLElement & {
webkitRequestFullscreen?: () => Promise<void> | void;
};
try {
if (document.fullscreenElement || (document as typeof document & { webkitFullscreenElement?: Element | null }).webkitFullscreenElement) {
if (document.exitFullscreen) {
await document.exitFullscreen();
} else if ((document as typeof document & { webkitExitFullscreen?: () => Promise<void> | void }).webkitExitFullscreen) {
await (document as typeof document & { webkitExitFullscreen?: () => Promise<void> | void }).webkitExitFullscreen?.();
}
} else if (root.requestFullscreen) {
await root.requestFullscreen();
} else if (root.webkitRequestFullscreen) {
await root.webkitRequestFullscreen();
}
} catch {
// Ignore fullscreen errors.
}
updateFullscreenButtonVisibility();
});
document.addEventListener('fullscreenchange', () => {
updateFullscreenButtonVisibility();
});
document.addEventListener('webkitfullscreenchange', () => {
updateFullscreenButtonVisibility();
});
overlayPanel?.addEventListener('scroll', () => {
syncTouchPreviewVisibility();
});
gamepadCalibrationButton?.addEventListener('click', () => {
startGamepadCalibration();
});

View File

@@ -550,7 +550,7 @@ button:disabled {
top: 12px;
left: 50%;
transform: translateX(-50%);
width: min(420px, 92vw);
width: min(220px, 48vw);
height: 40px;
padding: 6px 16px;
border-radius: 999px;
@@ -565,3 +565,12 @@ button:disabled {
z-index: 9;
backdrop-filter: blur(6px);
}
.fullscreen-button {
position: fixed;
top: 8px;
right: 8px;
z-index: 12;
padding: 6px 10px;
font-size: 11px;
}