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

另見