MVP checks / mvp (push) Waiting to run
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.
116 lines
3.4 KiB
JavaScript
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);
|
|
}
|
|
}
|