API Pointer Lock

Pointer Lock (antes llamado Bloqueo del Mouse) proporciona métodos de entrada basados en el movimiento del ratón a lo largo del tiempo (conocido como deltas), no sólo en la posición absoluta del cursor del mouse en la ventana gráfica. Te da acceso al movimiento puro del mouse, bloquea el objetivo de los eventos del mouse en un único elemento, elimina los límites sobre la distancia que puede alcanzar el movimiento del mouse en una única dirección y elimina el cursor de la vista. Es ideal para juegos 3D en primera persona, por ejemplo.

Además, la API es útil para cualquier aplicación que requiera una entrada significativa del mouse para controlar movimientos, rotar objetos y cambiar entradas. Por ejemplo, permite a los usuarios controlar el ángulo de visión moviendo el mouse sin necesidad de pulsar ningún botón. Así, los botones quedan libres para otras acciones. Otros ejemplos son las aplicaciones para ver mapas o imágenes por satélite.

Pointer Lock te permite acceder a los eventos del ratón incluso cuando el cursor sobrepasa los límites del navegador o de la pantalla. Por ejemplo, sus usuarios pueden seguir rotando o manipulando un modelo 3D moviendo el mouse sin fin. Sin Pointer Lock, la rotación o manipulación se detiene en el momento en que el puntero alcanza el borde del navegador o la pantalla. Ahora, los jugadores pueden hacer clic en los botones y desplazar el cursor del mouse de un lado a otro sin preocuparse de abandonar el área de juego y hacer clic accidentalmente en otra aplicación que les quitaría el foco del ratón.

Conceptos básicos

Pointer Lock está relacionado con mouse capture. Mouse capture proporciona una entrega continua de eventos a un elemento de destino mientras se arrastra el mouse, pero se detiene cuando se suelta el botón del mouse. Pointer Lock se diferencia de la captura de mouse en lo siguiente:

  • Es persistente: Pointer Lock no libera el mouse hasta que se realiza una llamada explícita a la API o el usuario utiliza un gesto de liberación específico.
  • No está limitado por los límites del navegador o de la pantalla.
  • Continúa enviando eventos independientemente del estado del botón del mouse.
  • Oculta el cursor.

Descripción de métodos/propiedades

Esta sección proporciona una breve descripción de cada propiedad y método relacionado con la especificación de bloqueo de puntero.

requestPointerLock()

La API Pointer Lock, similar a la Fullscreen API, amplía los elementos DOM añadiendo un nuevo método, requestPointerLock(). El siguiente ejemplo solicita el bloqueo de puntero en un elemento <canvas>:

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

Nota: Si un usuario ha salido del bloqueo de puntero mediante el default unlock gesture, o el bloqueo de puntero no ha sido introducido previamente para este documento, un evento generado como resultado de un gesto de compromiso debe ser recibido por el documento antes de que requestPointerLock tenga éxito. (de https://w3c.github.io/pointerlock/#extensions-to-the-element-interface)

Los sistemas operativos activan la aceleración del ratón por defecto, lo que resulta útil cuando a veces se desea un movimiento lento y preciso (piensa en que podrías utilizar un paquete de gráficos), pero también quieres mover grandes distancias con un movimiento más rápido del ratón (piensa en el desplazamiento y la selección de varios archivos). Sin embargo, para algunos juegos de perspectiva en primera persona, se prefieren los datos de entrada brutos del ratón para controlar la rotación de la cámara, donde el mismo movimiento de distancia, rápido o lento, da como resultado la misma rotación. Según los jugadores profesionales, esto mejora la experiencia de juego y aumenta la precisión.

Para desactivar la aceleración del ratón a nivel de sistema operativo y acceder a la entrada sin procesar del ratón, puedes establecer unadjustedMovement a true:

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

Gestión de las versiones con y sin promesa de requestPointerLock()

El fragmento de código anterior seguirá funcionando en navegadores que no soporten la versión basada en promesas de requestPointerLock() o la opción unadjustedMovement — el operador await está permitido delante de una función que no devuelva una promesa, y el objeto options simplemente será ignorado en los navegadores que no lo soporten.

Sin embargo, esto podría ser confuso, y tiene otros posibles efectos secundarios (por ejemplo, tratar de utilizar requestPointerLock().then() arrojaría un error en los navegadores que no lo soportan), por lo que es posible que desee manejar esto explícitamente utilizando código en las siguientes líneas:

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

  if (!promise) {
    console.log("no se admite la desactivación de la aceleración del mouse");
    return;
  }

  return promise
    .then(() => console.log("el puntero está bloqueado"))
    .catch((error) => {
      if (error.name === "NotSupportedError") {
        // Es posible que algunas plataformas no admitan el movimiento no ajustado.
        // Puede solicitar de nuevo un bloqueo de puntero normal.
        return myTargetElement.requestPointerLock();
      }
    });
}

pointerLockElement y exitPointerLock()

La API de bloqueo de puntero también amplía la interfaz Document, añadiendo una nueva propiedad y un nuevo método:

La propiedad pointerLockElement es útil para determinar si algún elemento está actualmente bloqueado por puntero (por ejemplo, para hacer una comprobación booleana) y también para obtener una referencia al elemento bloqueado, si existe.

He aquí un ejemplo de utilización de pointerLockElement:

js
if (document.pointerLockElement === canvas) {
  console.log("El estado de bloqueo del puntero ahora está bloqueado");
} else {
  console.log("El estado de bloqueo del puntero ahora está desbloqueado");
}

El método Document.exitPointerLock() se utiliza para salir del bloqueo de puntero, y al igual que requestPointerLock, funciona de forma asíncrona utilizando los eventos pointerlockchange y pointerlockerror, de los que se hablará más adelante.

js
document.exitPointerLock();

Evento pointerlockchange

Cuando cambia el estado de bloqueo del puntero —por ejemplo, al llamar a requestPointerLock() o exitPointerLock(), al pulsar el usuario la tecla ESC, etc.— se envía al document el evento pointerlockchange. Se trata de un evento simple que no contiene datos adicionales.

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

function lockChangeAlert() {
  if (document.pointerLockElement === canvas) {
    console.log("El estado de bloqueo del puntero ahora está bloqueado");
    // Haz algo útil como respuesta
  } else {
    console.log("El estado de bloqueo del puntero ahora está desbloqueado");
    // Haz algo útil como respuesta
  }
}

Evento pointerlockerror

Cuando se produce un error al llamar a requestPointerLock() o exitPointerLock(), se envía el evento pointerlockerror al document. Se trata de un evento simple que no contiene datos adicionales.

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

function lockError(e) {
  alert("El bloqueo del puntero falló");
}

Extensiones a eventos de ratón

La API de bloqueo de puntero amplía la interfaz normal MouseEvent con atributos de movimiento. Dos nuevos atributos para eventos de ratón —movementX y movementY— proporcionan el cambio en las posiciones del ratón. Los valores de los parámetros son los mismos que la diferencia entre los valores de las propiedades MouseEvent, screenX y screenY, que se almacenan en dos eventos posteriores mousemove, eNow y ePrevious. En otras palabras, el parámetro de bloqueo del puntero movementX = eNow.screenX - ePrevious.screenX.

Estado de bloqueo

Cuando el bloqueo de puntero está activado, las propiedades MouseEvent estándar clientX, clientY, screenX y screenY se mantienen constantes, como si el ratón no se moviera. Las propiedades movementX y movementY siguen proporcionando el cambio de posición del ratón. No hay límite para los valores movementX y movementY si el ratón se mueve continuamente en una única dirección. El concepto de cursor del ratón no existe y el cursor no puede moverse fuera de la ventana ni ser sujetado por un borde de la pantalla.

Estado desbloqueado

Los parámetros movementX y movementY son válidos independientemente del estado de bloqueo del ratón, y están disponibles incluso cuando está desbloqueado para mayor comodidad.

Cuando el ratón está desbloqueado, el cursor del sistema puede salir y volver a entrar en la ventana del navegador. Si eso ocurre, movementX y movementY podrían ponerse a cero.

Ejemplo sencillo de recorrido

Hemos escrito una demo de bloqueo de puntero (ver código fuente) para mostrarle cómo utilizarlo para configurar un sistema de control sencillo. Esta demo utiliza JavaScript para dibujar una bola encima de un elemento <canvas>. Al hacer clic en el lienzo, se utiliza el bloqueo del puntero para eliminar el puntero del ratón y permitirle mover la bola directamente con el ratón. Veamos cómo funciona esto.

Establecemos las posiciones iniciales x e y en el lienzo:

js
let x = 50;
let y = 50;

A continuación configuramos un detector de eventos para que ejecute el método requestPointerLock() en el lienzo cuando se haga clic sobre él, lo que iniciará el bloqueo del puntero. La comprobación de document.pointerLockElement es para ver si ya hay un bloqueo de puntero activo - no queremos seguir llamando a requestPointerLock() en el lienzo cada vez que hacemos clic dentro de él si ya tenemos un bloqueo de puntero.

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

Nota: El fragmento anterior funciona en navegadores que no soportan la versión promise de requestPointerLock(). Ver Manejo de versiones promise y no-promise de requestPointerLock() para una explicación.

Ahora vamos a detectar el evento de bloqueo de puntero: pointerlockchange. Cuando esto ocurre, ejecutamos una función llamada lockChangeAlert() para manejar el cambio.

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

Esta función comprueba la propiedad pointerLockElement para ver si es nuestro canvas. Si es así, adjunta un listener de eventos para manejar los movimientos del ratón con la función updatePosition(). Si no es así, elimina el listener de nuevo.

js
function lockChangeAlert() {
  if (document.pointerLockElement === canvas) {
    console.log("El estado de bloqueo del puntero ahora está bloqueado");
    document.addEventListener("mousemove", updatePosition, false);
  } else {
    console.log("El estado de bloqueo del puntero ahora está desbloqueado");
    document.removeEventListener("mousemove", updatePosition, false);
  }
}

La función updatePosition() actualiza la posición de la bola en el lienzo (x e y), y también incluye sentencias if () para comprobar si la bola se ha salido de los bordes del lienzo. Si es así, hace que la bola se desplace hasta el borde opuesto. También incluye una comprobación de si se ha hecho previamente una llamada a requestAnimationFrame(), y si es así, la llama de nuevo si es necesario, y llama a la función canvasDraw() que actualiza la escena del lienzo. También se configura un rastreador para escribir los valores X e Y en la pantalla, como referencia.

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();
    });
  }
}

La función canvasDraw() dibuja la bola en las posiciones x e y actuales:

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();
}

Limitaciones de IFrame

Pointer Lock sólo puede bloquear un <iframe> a la vez. Si bloqueas un <iframe>, no puedes bloquear otro y transferirle el objetivo; el bloqueo de puntero dará error. Para evitar esta limitación, desbloquea primero el <iframe> bloqueado, y luego bloquea el otro.

Mientras que <iframe> funciona por defecto, los <iframe> "aislados" bloquean el bloqueo de puntero. Para evitar esta limitación, utilice <iframe sandbox="allow-pointer-lock">.

Especificaciones

Specification
Pointer Lock 2.0

Compatibilidad con navegadores

api.Document.exitPointerLock

BCD tables only load in the browser

api.Element.requestPointerLock

BCD tables only load in the browser

Véase también