/** Keep pointer capture retryable after browser errors and ordinary unlocks. */ export class PointerLockController { constructor(canvas, { onChange = () => {}, onError = () => {} } = {}) { this.canvas = canvas; this.document = canvas.ownerDocument; this.onChange = onChange; this.onError = onError; this.status = this.locked ? "locked" : "idle"; this.lastError = null; this.generation = 0; this.disposed = false; this.cancelled = false; this.observedLocked = this.locked; this.changeListener = () => this.changed(); this.errorListener = (event) => this.failed( event.error || new Error(event.message || "Pointer lock was denied by the browser."), this.generation, ); this.document.addEventListener("pointerlockchange", this.changeListener); this.document.addEventListener("pointerlockerror", this.errorListener); } get locked() { return this.document.pointerLockElement === this.canvas; } request() { if (this.disposed || this.locked || this.status === "requesting") return false; const generation = ++this.generation; this.cancelled = false; this.lastError = null; this.status = "requesting"; if (typeof this.canvas.requestPointerLock !== "function") { const error = new Error("This browser does not support pointer lock."); error.name = "NotSupportedError"; this.failed(error, generation, "unsupported"); return false; } try { if (this.canvas.tabIndex < 0) this.canvas.tabIndex = 0; this.canvas.focus({ preventScroll: true }); // Start synchronously: awaiting anything here loses the user's gesture. const result = this.canvas.requestPointerLock(); result?.then?.( () => { if (this.cancelled && this.locked) this.document.exitPointerLock?.(); else if ( !this.disposed && generation === this.generation && this.locked ) this.changed(); }, (error) => this.failed(error, generation), ); return true; } catch (error) { this.failed(error, generation); return false; } } failed(error, generation, status = "error") { if ( this.disposed || generation !== this.generation || this.locked || this.status !== "requesting" ) return; this.generation++; this.status = status; this.lastError = error?.message ? error : new Error(String(error || "Pointer lock failed.")); this.onError(this.lastError); } changed() { if (this.disposed) return; if (this.locked && this.cancelled) { this.document.exitPointerLock?.(); return; } const locked = this.locked; this.generation++; this.status = locked ? "locked" : "idle"; this.lastError = null; if (locked !== this.observedLocked) { this.observedLocked = locked; this.onChange(locked); } } release() { if (this.disposed) return; const requesting = this.status === "requesting"; this.generation++; this.cancelled = true; this.status = "idle"; this.lastError = null; if (this.locked || requesting) this.document.exitPointerLock?.(); } dispose() { if (this.disposed) return; this.release(); this.disposed = true; this.document.removeEventListener("pointerlockchange", this.changeListener); this.document.removeEventListener("pointerlockerror", this.errorListener); } }