Pointer Lock API

深入理解 Pointer Lock API:鼠标锁定、movementX/Y、3D 游戏交互、fullscreen 游戏场景。

Pointer Lock API

一、Pointer Lock 概述

1.1 什么是 Pointer Lock

Pointer Lock API(曾名 Mouse Lock API)是一种输入机制,基于鼠标移动时间(deltas)而非绝对位置获取输入。它将鼠标事件锁定到单个元素,消除边界限制并隐藏光标。

// 请求指针锁定
canvas.addEventListener('click', async () => {
  await canvas.requestPointerLock();
});

1.2 与普通鼠标捕获的区别

特性 Pointer Lock 普通鼠标捕获
光标可见性 完全隐藏 可选择隐藏
移动边界 无限制 可限制在元素内
movementX/Y 累积增量 相对最后位置
用途 游戏、3D 应用 拖拽、绘图
// 普通鼠标捕获:光标仍在,事件重定向
element.addEventListener('mouseover', (e) => {
  // 事件重定向到 element
});

// Pointer Lock:完全锁定,光标消失
canvas.requestPointerLock();

二、核心 API

2.1 requestPointerLock

canvas.addEventListener('click', async () => {
  try {
    await canvas.requestPointerLock();
    console.log('Pointer locked');
  } catch (error) {
    console.error('Failed to lock pointer:', error);
  }
});

// 带选项
canvas.requestPointerLock({
  unadjustedMovement: true  // 禁用 OS 级鼠标加速
});

2.2 退出锁定

document.exitPointerLock();
// 监听退出
document.addEventListener('pointerlockchange', () => {
  if (document.pointerLockElement === null) {
    console.log('Pointer unlocked');
  }
});

2.3 pointerLockElement 属性

if (document.pointerLockElement === canvas) {
  console.log('Pointer is locked to canvas');
} else {
  console.log('Pointer is not locked');
}

三、鼠标移动事件

3.1 movementX 和 movementY

Pointer Lock 期间,mousemove 事件的 movementXmovementY 属性表示自上次事件以来的累积鼠标移动:

let x = 0, y = 0;

document.addEventListener('mousemove', (e) => {
  // 累积移动量
  x += e.movementX;
  y += e.movementY;

  // 更新相机/玩家位置
  camera.rotation.x += e.movementY * 0.002;
  camera.rotation.y += e.movementX * 0.002;
});

3.2 3D 相机控制

class FPSCamera {
  constructor(element) {
    this.element = element;
    this.pitch = 0;  // 垂直旋转
    this.yaw = 0;    // 水平旋转
    this.sensitivity = 0.002;

    document.addEventListener('mousemove', this.onMouseMove.bind(this));
  }

  onMouseMove(e) {
    if (document.pointerLockElement !== this.element) return;

    this.yaw -= e.movementX * this.sensitivity;
    this.pitch -= e.movementY * this.sensitivity;

    // 限制垂直旋转
    this.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.pitch));
  }

  lookAt(target) {
    // 应用旋转到相机
  }
}

四、实际应用

4.1 3D 场景查看器

class SceneViewer {
  constructor(canvas) {
    this.canvas = canvas;
    this.isLocked = false;

    canvas.addEventListener('click', () => this.enter());
    document.addEventListener('pointerlockchange', () => this.onLockChange());
    document.addEventListener('mousemove', (e) => this.onMouseMove(e));
  }

  async enter() {
    await this.canvas.requestPointerLock();
  }

  onLockChange() {
    this.isLocked = document.pointerLockElement === this.canvas;
    this.canvas.style.cursor = this.isLocked ? 'none' : 'pointer';
  }

  onMouseMove(e) {
    if (!this.isLocked) return;

    // 更新场景旋转
    this.scene.rotation.y -= e.movementX * 0.01;
    this.scene.rotation.x -= e.movementY * 0.01;
  }
}

4.2 绘图应用

class DrawingApp {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.isDrawing = false;

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

    document.addEventListener('mousemove', (e) => {
      if (document.pointerLockElement !== canvas) return;
      if (e.buttons === 1) {
        this.ctx.lineTo(e.clientX, e.clientY);
        this.ctx.stroke();
      }
    });

    canvas.addEventListener('mousedown', (e) => {
      if (document.pointerLockElement === canvas) {
        this.ctx.beginPath();
        this.ctx.moveTo(e.clientX, e.clientY);
      }
    });
  }
}

五、权限与安全

5.1 用户交互要求

Pointer Lock 需要用户交互(点击)才能激活,这是浏览器的安全要求:

// ❌ 直接调用会失败
canvas.requestPointerLock(); // 错误:需要在用户手势中调用

// ✅ 在点击事件中调用
canvas.addEventListener('click', async () => {
  await canvas.requestPointerLock();
});

5.2 退出锁定

用户可以通过 ESC 键退出 Pointer Lock:

document.addEventListener('pointerlockchange', () => {
  if (document.pointerLockElement === null) {
    // 用户主动退出或按下 ESC
    console.log('Pointer lock released');
  }
});

5.3 unadjustedMovement 选项

// 获取"原始"鼠标移动,不受 OS 加速影响
canvas.addEventListener('click', async () => {
  await canvas.requestPointerLock({
    unadjustedMovement: true
  });
});

六、面试高频问题

Q: Pointer Lock 和普通鼠标事件有什么区别?

回答要点:普通鼠标事件基于绝对位置,每次事件报告相对于视口的位置;Pointer Lock 隐藏光标,mousemove 事件的 movementX/Y 报告自上次事件以来的累积增量,适合需要无限移动的场景如 3D 游戏。

Q: 什么场景适合使用 Pointer Lock?

回答要点:3D 游戏(FPS 视角控制)、3D 场景查看器、绘图应用、需要精确鼠标控制的任何场景。Pointer Lock 解决了普通鼠标事件在边界处停止的问题。

Q: 为什么 Pointer Lock 需要用户点击才能激活?

回答要点:浏览器安全策略要求——不允许网页在未经用户同意的情况下隐藏光标并拦截鼠标输入,这会使用户无法退出或与其他应用交互。


参考资料

延展阅读