From a92b05a0cda0c3bc5d10a70dd39cb62c268a298a Mon Sep 17 00:00:00 2001 From: webdevcody Date: Tue, 29 Sep 2026 20:42:59 -0400 Subject: [PATCH] Hold the view still when a click gives the mouse back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sending a worker home ends on a click on Send home. Closing the window takes the mouse straight back for looking around, at the moment the hand that clicked is often already moving on, so the pointer vanished under it and the rest of the move swung the camera somewhere else. A window closed with the mouse (Send home, ✕, the backdrop) still takes it back at once, as closing by ✕ or Esc must, but the view now holds still until the mouse has rested for 250 ms since it was taken (1 s at most). Closing with a key (Esc, Enter) and clicking the office to look around turn the view straight away as before. Co-Authored-By: Claude Opus 5.5 --- src/client/main.ts | 7 ++++++- src/client/player.ts | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/client/main.ts b/src/client/main.ts index e455073e..c9620cac 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -4122,6 +4122,10 @@ onDoingChange(() => sendDoing()); * takes it instead (a key counts for the browser, where the Esc that closed the window doesn't). */ let relookOnKey = false; +/** Whether the last thing you pressed was a mouse button rather than a key (see backToGame). Captured, before a window acts on it. */ +let pressedMouse = false; +window.addEventListener('pointerdown', () => (pressedMouse = true), true); +window.addEventListener('keydown', () => (pressedMouse = false), true); onModalChange((open) => { if (open) telescope.exit(); player.enabled = !open; @@ -4155,7 +4159,8 @@ function backToGame() { // The browser lets a page re-capture the mouse it let go of itself (see yieldMouse), even on Esc // (which it doesn't count as a click or key), and any time after a click, like one on ✕. When it // won't (nothing of yours opened the window, or a stricter browser), the next key you press does. - player.lock(); + // Closed with a click (Send home, ✕), the view waits for the hand that clicked to come to rest. + player.lock(pressedMouse); relookOnKey = true; } document.addEventListener('pointerlockchange', () => { diff --git a/src/client/player.ts b/src/client/player.ts index 54ec2005..ee9a51da 100644 --- a/src/client/player.ts +++ b/src/client/player.ts @@ -20,6 +20,12 @@ export const HIPS = 0.42; const GET_UP = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space']; const LOOK_SPEED = 0.0022; // radians per pixel of mouse movement while the pointer is locked const DRAG_LOOK_SPEED = 0.005; +/** + * Taking the mouse back from a click (see lock's `settle`): how long it must rest once it's taken + * before it looks around, in ms, and how long at most the view is held still for. + */ +const SETTLE_REST = 250; +const SETTLE_MAX = 1000; const CENTER = new THREE.Vector2(0, 0); export class PlayerController { @@ -102,6 +108,11 @@ export class PlayerController { private escDownAt = 0; /** Asked for while Esc was down: taken once it comes up (see lock). */ private lockOnEscUp = false; + /** The lock asked for is to settle (see lock), and until when (ms) a lock that landed so still is. */ + private settleNext = false; + private settleUntil = 0; + /** When the mouse last moved, or a lock that settles landed. */ + private movedAt = 0; enabled = true; /** False while the mouse picks something else (an emote on the wheel), so it doesn't turn the camera. */ mouseLook = true; @@ -166,10 +177,18 @@ export class PlayerController { } }); window.addEventListener('pointermove', (e) => { + const now = performance.now(); + const rested = now - this.movedAt; + this.movedAt = now; if (!this.mouseLook) return; if (this.locked) { // Held for a moment under a window (see yieldMouse), the mouse doesn't turn your head. if (!this.enabled) return; + // Taken back from a click, the hand that clicked may be moving on still: that isn't looking around. + if (this.settleUntil) { + if (rested < SETTLE_REST && now < this.settleUntil) return; + this.settleUntil = 0; + } // Some platforms report a bogus huge jump right after locking. const clamp = (v: number) => THREE.MathUtils.clamp(v, -250, 250); this.look(clamp(e.movementX) * LOOK_SPEED, clamp(e.movementY) * LOOK_SPEED); @@ -199,6 +218,10 @@ export class PlayerController { } this.everLocked = true; this.drag = null; + // The pause to click doesn't count as the hand coming to rest: only once it's taken. + if (this.settleNext) this.movedAt = performance.now(); + this.settleUntil = this.settleNext ? this.movedAt + SETTLE_MAX : 0; + this.settleNext = false; // A lock that lands with a window open (the one yieldMouse takes, or a relock racing the next window) is let go. if (!this.enabled) this.unlock(); }); @@ -270,8 +293,13 @@ export class PlayerController { return this.enabled && codes.some((c) => this.keys.has(c)); } - /** Captures the mouse for looking around, as the first click on the scene does. */ - lock() { + /** + * Captures the mouse for looking around, as the first click on the scene does. With `settle` (a + * click just closed a window), the view holds still until the mouse comes to rest, so the rest of + * the hand's move doesn't swing it somewhere else. + */ + lock(settle = false) { + this.settleNext = settle; // Still being let go of, for a window that closed again at once: taken back once it's free. if (this.locked && this.letting) this.lockAfter = true; if (this.locked || this.lockPending) return;