Pointer Lock API

Pointer Lock API(以前称为 Mouse Lock API)提供基于鼠标随时间移动(即差值)的输入方法,而不仅仅是鼠标光标在视口中的绝对位置。它可以访问原始鼠标移动,将鼠标事件的目标锁定到单个元素,消除鼠标单方向移动距离的限制,并隐藏光标。它非常适合第一人称 3D 游戏等应用。

更重要的是,该 API 对于任何需要大量鼠标输入来控制移动、旋转对象和更改输入的应用程序都很有用,例如,允许用户通过移动鼠标进行控制,而无需单击任何按钮。这样,按钮就可以用于其他操作。其他示例包括地图或卫星图像查看应用程序。

Pointer lock 允许您即使在光标超出浏览器或屏幕边界时也能访问鼠标事件。例如,您的用户可以通过持续移动鼠标来旋转或操作 3D 模型。如果没有 Pointer lock,旋转或操作会在指针到达浏览器或屏幕边缘时停止。游戏玩家现在可以单击按钮,来回滑动鼠标光标,而无需担心离开游戏区域并意外单击另一个应用程序,从而将鼠标焦点从游戏中移开。

基本概念

Pointer lock 与 pointer capture 相关。Pointer capture 在拖动鼠标时持续将事件传递给目标元素,但在释放鼠标按钮时停止。Pointer lock 与 pointer capture 的区别如下:

  • 它具有持久性:Pointer lock 在进行明确的 API 调用或用户使用特定的释放手势之前,不会释放鼠标。
  • 它不受浏览器或屏幕边界的限制。
  • 无论鼠标按钮状态如何,它都会继续发送事件。
  • 它会隐藏光标。

方法/属性概述

本节简要介绍与 pointer lock 规范相关的每个属性和方法。

requestPointerLock()

Pointer lock API 类似于 Fullscreen API,通过添加一个新方法 requestPointerLock() 来扩展 DOM 元素。以下示例在 <canvas> 元素上请求 pointer lock。

js
canvas.addEventListener("click", async () => {
  await canvas.requestPointerLock();
});

注意: 如果用户通过 默认解锁手势 退出了 pointer lock,或者该文档之前没有进入 pointer lock,则在 requestPointerLock 成功之前,文档必须收到由 参与手势 生成的事件。(来自 https://w3c.github.io/pointerlock/#extensions-to-the-element-interface

操作系统默认启用鼠标加速,这在您有时需要缓慢精确移动(例如使用图形软件包时),但也想通过更快的鼠标移动来移动大距离(例如滚动和选择多个文件时)时很有用。然而,对于某些第一人称视角游戏,原始鼠标输入数据对于控制相机旋转更为可取——即,相同距离的移动,无论快慢,都会产生相同的旋转。根据专业游戏玩家的说法,这可以带来更好的游戏体验和更高的准确性。

要禁用操作系统级别的鼠标加速并访问原始鼠标输入,您可以将 unadjustedMovement 设置为 true

js
canvas.addEventListener("click", async () => {
  await canvas.requestPointerLock({
    unadjustedMovement: true,
  });
});

处理 requestPointerLock() 的 Promise 和非 Promise 版本

上述代码片段在不支持 requestPointerLock() 的 Promise 版本或 unadjustedMovement 选项的浏览器中仍然有效——await 运算符可以放在不返回 Promise 的函数前面,而在不支持的浏览器中,options 对象将被忽略。

然而,这可能会令人困惑,并且存在其他潜在的副作用(例如,在不支持的浏览器中尝试使用 requestPointerLock().then() 会抛出错误),因此您可能希望使用类似以下的代码来显式处理:

js
function requestPointerLockWithUnadjustedMovement() {
  const promise = myTargetElement.requestPointerLock({
    unadjustedMovement: true,
  });

  if (!promise) {
    console.log("disabling mouse acceleration is not supported");
    return;
  }

  return promise
    .then(() => console.log("pointer is locked"))
    .catch((error) => {
      if (error.name === "NotSupportedError") {
        // Some platforms may not support unadjusted movement.
        // You can request again a regular pointer lock.
        return myTargetElement.requestPointerLock();
      }
    });
}

pointerLockElement 和 exitPointerLock()

Pointer lock API 还扩展了 Document 接口,添加了一个新属性和一个新方法:

pointerLockElement 属性对于确定是否有元素当前被 pointer lock(例如,用于布尔检查)以及获取对被锁定元素(如果有)的引用非常有用。

这是使用 pointerLockElement 的示例:

js
if (document.pointerLockElement === canvas) {
  console.log("The pointer lock status is now locked");
} else {
  console.log("The pointer lock status is now unlocked");
}

Document.exitPointerLock() 方法用于退出 pointer lock,并且与 requestPointerLock 一样,它使用 pointerlockchangepointerlockerror 事件异步工作,您稍后会看到更多关于这些事件的内容。

js
document.exitPointerLock();

pointerlockchange 事件

当 Pointer lock 状态发生变化时——例如,当调用 requestPointerLock()exitPointerLock()、用户按下 ESC 键等——pointerlockchange 事件会被分派到 document。这是一个不包含额外数据的简单事件。

js
document.addEventListener("pointerlockchange", lockChangeAlert);

function lockChangeAlert() {
  if (document.pointerLockElement === canvas) {
    console.log("The pointer lock status is now locked");
    // Do something useful in response
  } else {
    console.log("The pointer lock status is now unlocked");
    // Do something useful in response
  }
}

pointerlockerror 事件

当调用 requestPointerLock()exitPointerLock() 导致错误时,pointerlockerror 事件会被分派到 document。这是一个不包含额外数据的简单事件。

js
document.addEventListener("pointerlockerror", lockError);

function lockError(e) {
  alert("Pointer lock failed");
}

鼠标事件的扩展

Pointer lock API 使用 movement 属性扩展了标准的 MouseEvent 接口。鼠标事件的两个新属性——movementXmovementY——提供了鼠标位置的变化。这些参数的值与 MouseEvent 属性 screenXscreenY 的值之间的差异相同,这些差异存储在两个连续的 mousemove 事件 eNowePrevious 中。换句话说,Pointer lock 参数 movementX = eNow.screenX - ePrevious.screenX

锁定状态

当 Pointer lock 启用时,标准的 MouseEvent 属性 clientX, clientY, screenX, 和 screenY 会保持不变,仿佛鼠标没有移动一样。movementXmovementY 属性会继续提供鼠标位置的变化。如果鼠标在单个方向上持续移动,movementXmovementY 的值没有限制。鼠标光标的概念不存在,光标也不能移出窗口或被屏幕边缘限制。

解锁状态

无论鼠标锁定状态如何,movementXmovementY 参数都有效,并且为了方便起见,即使在解锁状态下也可以使用。

当鼠标解锁时,系统光标可以退出和重新进入浏览器窗口。如果发生这种情况,movementXmovementY 可能会被设置为零。

简单示例演示

我们编写了一个 pointer lock 演示查看源代码),向您展示如何使用它来设置一个简单的控制系统。此演示使用 JavaScript 在 <canvas> 元素之上绘制一个球。当您单击画布时,pointer lock 会被用于移除鼠标指针,并允许您直接通过鼠标移动球。让我们看看它是如何工作的。

我们在画布上设置初始的 x 和 y 位置。

js
let x = 50;
let y = 50;

接下来,我们设置一个事件监听器,在画布被单击时运行 requestPointerLock() 方法,从而启动 pointer lock。document.pointerLockElement 检查是为了查看是否已经有一个活动的 pointer lock——如果我们已经在画布内单击并获得了 pointer lock,我们不想每次都再次调用 requestPointerLock()

js
canvas.addEventListener("click", async () => {
  if (!document.pointerLockElement) {
    await canvas.requestPointerLock({
      unadjustedMovement: true,
    });
  }
});

注意: 上面的代码片段在不支持 requestPointerLock() Promise 版本的浏览器中也能正常工作。有关说明,请参阅 Handling promise and non-promise versions of requestPointerLock()

现在是专用的 pointer lock 事件监听器:pointerlockchange。当它发生时,我们会运行一个名为 lockChangeAlert() 的函数来处理变化。

js
document.addEventListener("pointerlockchange", lockChangeAlert);

此函数检查 pointerLockElement 属性,看它是否是我们的画布。如果是,它会附加一个事件监听器来使用 updatePosition() 函数处理鼠标移动。如果不是,它会再次移除事件监听器。

js
function lockChangeAlert() {
  if (document.pointerLockElement === canvas) {
    console.log("The pointer lock status is now locked");
    document.addEventListener("mousemove", updatePosition);
  } else {
    console.log("The pointer lock status is now unlocked");
    document.removeEventListener("mousemove", updatePosition);
  }
}

updatePosition() 函数更新画布上的球的位置(xy),还包括 if () 语句来检查球是否已超出画布边缘。如果是,它会让球绕到另一侧边缘。它还包括一个检查,看是否之前调用了 requestAnimationFrame(),如果调用了,则会再次按需调用它,并调用 canvasDraw() 函数来更新画布场景。还会设置一个跟踪器,将 X 和 Y 值写入屏幕,以供参考。

js
const tracker = document.getElementById("tracker");

let animation;
function updatePosition(e) {
  x += e.movementX;
  y += e.movementY;
  if (x > canvas.width + RADIUS) {
    x = -RADIUS;
  }
  if (y > canvas.height + RADIUS) {
    y = -RADIUS;
  }
  if (x < -RADIUS) {
    x = canvas.width + RADIUS;
  }
  if (y < -RADIUS) {
    y = canvas.height + RADIUS;
  }
  tracker.textContent = `X position: ${x}, Y position: ${y}`;

  animation ??= requestAnimationFrame(() => {
    animation = null;
    canvasDraw();
  });
}

canvasDraw() 函数在当前的 xy 位置绘制球。

js
function canvasDraw() {
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "red";
  ctx.beginPath();
  ctx.arc(x, y, RADIUS, 0, degToRad(360), true);
  ctx.fill();
}

IFrame 限制

Pointer lock 一次只能锁定一个 <iframe>。如果您锁定了一个 <iframe>,则无法锁定另一个并将其目标转移到它;pointer lock 会出错。要避免此限制,请先解锁已锁定的 <iframe>,然后再锁定另一个。

虽然 <iframe> 默认可用,但“沙盒化”的 <iframe> 会阻止 Pointer lock。要避免此限制,请使用 <iframe sandbox="allow-pointer-lock">

规范

规范
指针锁定 2.0

浏览器兼容性

api.Document.exitPointerLock

api.Element.requestPointerLock

另见