指针锁定 API

**指针锁定 API**(以前称为 *鼠标锁定 API*)提供基于鼠标随时间推移的移动(即增量)的输入方法,而不仅仅是鼠标光标在视口中的绝对位置。它使您可以访问原始鼠标移动,将鼠标事件的目标锁定到单个元素,消除鼠标移动在单个方向上可以移动多远的限制,并隐藏光标。例如,它非常适合第一人称 3D 游戏。

此外,该 API 对任何需要大量鼠标输入来控制移动、旋转对象和更改条目(例如,允许用户通过移动鼠标来控制视角而无需任何按钮点击)的应用程序都很有用。然后可以将按钮释放用于其他操作。其他示例包括用于查看地图或卫星图像的应用程序。

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

基本概念

指针锁定与指针捕获相关。指针捕获在拖动鼠标时继续将事件传递到目标元素,但当释放鼠标按钮时就会停止。指针锁定与指针捕获的不同之处在于

  • 它是持久的:指针锁定不会释放鼠标,直到发出显式 API 调用或用户使用特定的释放手势。
  • 它不受浏览器或屏幕边界的限制。
  • 无论鼠标按钮状态如何,它都会继续发送事件。
  • 它隐藏光标。

方法/属性概述

本节简要介绍了与指针锁定规范相关的每个属性和方法。

requestPointerLock()

指针锁定 API 类似于全屏 API,通过添加一个新方法requestPointerLock()来扩展 DOM 元素。以下示例请求对<canvas>元素进行指针锁定

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

注意:如果用户通过默认解锁手势退出了指针锁定,或者之前没有为该文档进入指针锁定,则必须在参与手势生成的事件由文档接收后,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 选项的浏览器中工作 - 允许在不返回 Promise 的函数前面使用await运算符,并且选项对象将在不支持的浏览器中被忽略。

但是,这可能会令人困惑,并且还有其他潜在的副作用(例如,尝试在不支持的浏览器中使用 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()

指针锁定 API 还扩展了Document接口,添加了一个新属性和一个新方法

pointerLockElement属性可用于确定任何元素当前是否处于指针锁定状态(例如,进行布尔检查),也可用于获取锁定元素的引用(如果有)。

以下是如何使用 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()方法用于退出指针锁定,并且与requestPointerLock类似,使用pointerlockchangepointerlockerror事件异步工作,您将在下面看到更多相关内容。

js
document.exitPointerLock();

pointerlockchange 事件

当指针锁定状态发生变化时 - 例如,当调用requestPointerLock()exitPointerLock(),用户按下 ESC 键等 - pointerlockchange事件将分派到 document。这是一个不包含任何额外数据的简单事件。

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

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, false);

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

鼠标事件的扩展

指针锁定 API 使用移动属性扩展了正常的MouseEvent接口。鼠标事件的两个新属性 - movementXmovementY - 提供鼠标位置的变化。参数的值与MouseEvent属性screenXscreenY的值之间的差值相同,这些值存储在两个后续的mousemove事件 eNowePrevious 中。换句话说,指针锁定参数 movementX = eNow.screenX - ePrevious.screenX

锁定状态

启用指针锁定后,标准MouseEvent属性clientXclientYscreenXscreenY保持不变,就像鼠标没有移动一样。movementXmovementY属性继续提供鼠标位置的变化。如果鼠标在一个方向上连续移动,则movementXmovementY值没有限制。鼠标光标的概念不存在,并且光标不能移出窗口或被屏幕边缘限制。

解锁状态

参数movementXmovementY无论鼠标锁定状态如何都始终有效,即使在解锁状态下出于方便起见也可以使用。

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

简单示例演练

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

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

js
let x = 50;
let y = 50;

接下来,我们设置一个事件侦听器,以便在点击画布时在画布上运行 requestPointerLock() 方法,从而启动指针锁定。document.pointerLockElement 检查用于查看是否已存在活动指针锁定 - 如果我们已经拥有指针锁定,我们不希望每次在画布内部点击时都继续在画布上调用 requestPointerLock()

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

注意:上述代码片段在不支持 requestPointerLock() 的 Promise 版本的浏览器中有效。有关说明,请参阅处理 requestPointerLock() 的 Promise 和非 Promise 版本

现在来看专用的指针锁定事件监听器:pointerlockchange。当此事件发生时,我们会运行一个名为 lockChangeAlert() 的函数来处理更改。

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

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

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

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}`;

  if (!animation) {
    animation = requestAnimationFrame(() => {
      animation = null;
      canvasDraw();
    });
  }
}

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

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

IFrame 限制

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

虽然 <iframe> 默认情况下可以工作,但“沙盒” <iframe> 会阻止指针锁定。为避免此限制,请使用 <iframe sandbox="allow-pointer-lock">

规范

规范
指针锁定 2.0

浏览器兼容性

api.Document.exitPointerLock

BCD 表格仅在浏览器中加载

api.Element.requestPointerLock

BCD 表格仅在浏览器中加载

另请参阅