Files
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

116 lines
3.4 KiB
JavaScript

/** 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);
}
}