diff --git a/src/react-components/preferences-screen.js b/src/react-components/preferences-screen.js index 9d577c57c0..b14bb71794 100644 --- a/src/react-components/preferences-screen.js +++ b/src/react-components/preferences-screen.js @@ -429,6 +429,14 @@ const preferenceLabels = defineMessages({ id: "preferences-screen.preference.disable-strafing", defaultMessage: "Disable strafing" }, + enablePointerlock: { + id: "preferences-screen.preference.enable-pointerlock", + defaultMessage: "Enable locking mouse, which allows you to move camera infinitely" + }, + enablePointerlockRawInput: { + id: "preferences-screen.preference.enable-pointerlock-raw-input", + defaultMessage: "When locking mouse is enabled, disable acceleration" + }, disableTeleporter: { id: "preferences-screen.preference.disable-teleporter", defaultMessage: "Disable teleporter" @@ -1083,6 +1091,14 @@ class PreferencesScreen extends Component { key: "disableTeleporter", prefType: PREFERENCE_LIST_ITEM_TYPE.CHECK_BOX }, + { + key: "enablePointerlock", + prefType: PREFERENCE_LIST_ITEM_TYPE.CHECK_BOX + }, + { + key: "enablePointerlockRawInput", + prefType: PREFERENCE_LIST_ITEM_TYPE.CHECK_BOX + }, { key: "movementSpeedModifier", prefType: PREFERENCE_LIST_ITEM_TYPE.NUMBER_WITH_RANGE, diff --git a/src/storage/store.js b/src/storage/store.js index 9484319356..0f0ecc9ddd 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -145,6 +145,8 @@ export const SCHEMA = { disableStrafing: { type: "bool", default: false }, disableTeleporter: { type: "bool", default: false }, disableAutoPixelRatio: { type: "bool", default: false }, + enablePointerlock: { type: "bool", default: false }, + enablePointerlockRawInput: { type: "bool", default: false }, movementSpeedModifier: { type: "number", default: 1 }, disableEchoCancellation: { type: "bool", default: isFirefoxReality }, disableNoiseSuppression: { type: "bool", default: isFirefoxReality }, diff --git a/src/systems/userinput/devices/mouse.js b/src/systems/userinput/devices/mouse.js index 340a6470b4..a18d7f4507 100644 --- a/src/systems/userinput/devices/mouse.js +++ b/src/systems/userinput/devices/mouse.js @@ -51,9 +51,10 @@ export class MouseDevice { }, { passive: false } ); + this.lockedInPos = [0, 0]; } - process(event) { + process(/** @type {MouseEvent & {target: HTMLElement}} */ event) { if (event.type === "wheel") { this.wheel += (event.deltaX + event.deltaY) / modeMod[event.deltaMode]; return true; @@ -62,36 +63,90 @@ export class MouseDevice { const left = event.button === 0; const middle = event.button === 1; const right = event.button === 2; - // Note: This assumes the canvas always starts in the top left. - // This works with the current sidebar and toolbar layout. - this.coords[0] = (event.clientX / this.canvas.clientWidth) * 2 - 1; - this.coords[1] = -(event.clientY / this.canvas.clientHeight) * 2 + 1; - this.movementXY[0] += event.movementX; - this.movementXY[1] += event.movementY; - if (event.type === "mousedown" && left) { - this.mouseDownLeftThisFrame = true; - this.buttonLeft = true; - } else if (event.type === "mousedown" && right) { - this.mouseDownRightThisFrame = true; - this.buttonRight = true; - } else if (event.type === "mousedown" && middle) { - this.mouseDownMiddleThisFrame = true; - this.buttonMiddle = true; - } else if (event.type === "mouseup" && left) { - if (this.mouseDownLeftThisFrame) { - return false; + + // not interested in other buttons like back/forward or 3rd, 4rd... side buttons + if (event.button > 2) { + return true; + } + + if (event.type === "mousemove") { + this.movementXY[0] += event.movementX; + this.movementXY[1] += event.movementY; + + if (document.pointerLockElement) { + // Note: This assumes the canvas always starts in the top left. + // This works with the current sidebar and toolbar layout. + this.coords[0] = (this.lockedInPos[0] / this.canvas.clientWidth) * 2 - 1; + this.coords[1] = -(this.lockedInPos[1] / this.canvas.clientHeight) * 2 + 1; + + this.lockedInPos[0] += event.movementX; + this.lockedInPos[1] += event.movementY; + } else { + this.coords[0] = (event.clientX / this.canvas.clientWidth) * 2 - 1; + this.coords[1] = -(event.clientY / this.canvas.clientHeight) * 2 + 1; } - this.buttonLeft = false; - } else if (event.type === "mouseup" && right) { - if (this.mouseDownRightThisFrame) { - return false; + } + + if (event.type === "mousedown") { + if (middle) { + this.mouseDownMiddleThisFrame = true; + this.buttonMiddle = true; + } else { + let setMouseDown = true; + if (window.APP.store.state.preferences.enablePointerlock) { + if (!document.pointerLockElement) { + const promise = event.target.requestPointerLock({ + unadjustedMovement: window.APP.store.state.preferences.enablePointerlockRawInput + }); + if (!promise) { + console.log("disabling mouse acceleration is not supported"); + } + + event.target.addEventListener( + "pointerlockchange", + () => { + if (!document.pointerLockElement) { + this[left ? "buttonLeft" : "buttonRight"] = false; + } + }, + { + once: true + } + ); + + this.lockedInPos = [event.clientX, event.clientY]; + } else { + setMouseDown = false; + } + } + + if (setMouseDown) { + this[left ? "mouseDownLeftThisFrame" : "mouseDownRightThisFrame"] = true; + this[left ? "buttonLeft" : "buttonRight"] = true; + } } - this.buttonRight = false; - } else if (event.type === "mouseup" && middle) { - if (this.mouseDownMiddleThisFrame) { - return false; + } + + if (event.type === "mouseup") { + if (document.pointerLockElement) { + document.exitPointerLock(); + } + if (left) { + if (this.mouseDownLeftThisFrame) { + return false; + } + this.buttonLeft = false; + } else if (right) { + if (this.mouseDownRightThisFrame) { + return false; + } + this.buttonRight = false; + } else if (middle) { + if (this.mouseDownMiddleThisFrame) { + return false; + } + this.buttonMiddle = false; } - this.buttonMiddle = false; } return true; }