HTML5: Use the pointer lock API to restrict mouse movement to an element

9 minute read

In this article we’ll look a bit closer at one of the new web APIs that recently has landed in Chrome: the pointer lock API (http://www.w3.org/TR/pointerlock/). With this set API it is possible to lock the mouse on a specific HTML element. With the mouse locked, you can move your mouse all around, and it will never leave the focus of the element. Great for games, 3D representations and probably a lot of other things. The definition from the mozilla site nicely explains this API:

"Pointer Lock (formerly called mouse lock) provides input methods based on the movement of the mouse over time (i.e., deltas), not just the absolute position of the mouse cursor. It gives you access to raw mouse movement, locks the target of mouse events to a single element, eliminates limits on how far mouse movement can go in a single direction, and removes the cursor from view."

A note for those of you using Firefox. Firefox does have an implementation of the Pointer lock API. But when you try out the examples on this page you’ll notice that it won’t work. The reason is that, at the time of writing, the pointer lock API in firefox is tied into the full screen API. So for pointer lock to work on firefox, the element you want to lock the pointer on, has to be in fullscreen mode. Once that requirement is lifted, the example on this page, will also work on firefox.

Demo time

To demonstrate this API I created a simple example:

PointerLock v. Non-PointerLock.png (click the image to run the demo)

This example shows two canvas elements. The first canvas element, when you move your mouse over it, shows a spaceship on a starry background. Using CSS the cursor is hidden and the spaceship follows your mouse. We simulate movement by using a sprite sheet to shows different ships. What you’ll notice is that when you move your mouse to far, the element loses focus and the ship stops moving and the stars stop updating: the mouse isn’t locked to the element.

When you click on the second element, assuming you use chrome (b.t.w I’m using version 24.0.1284.2 dev), you’ll see the popup shown in the previous screenshot.

Your mouse pointer is now locked to the element and you can move your mouse around all you want, and it’ll stay locked on the canvas updating the spaceship and the starry background. Hit ‘esc’ to release the pointer.

I’ll work you throught the steps you need to take to recreate this demo:

  • Add event listener for the pointerlockchange event
  • Register the onclick on the canvas element
  • Handle the callback when the pointer is locked
  • Draw the canvas based on mouse location and movement
  • Handle the callback when the pointer is unlocked

Add event listener for the pointerlockchange event

The first thing we’re going to do is register a callback handler for the pointerlock change event. Since this API is still in it’s early days we have to prefix the calls with ‘moz’ or ‘webkit’. To register the callback we use the following code:

        // register the callback when a pointerlock event occurs
        document.addEventListener('pointerlockchange', changeCallback, false);
        document.addEventListener('mozpointerlockchange', changeCallback, false);
        document.addEventListener('webkitpointerlockchange', changeCallback, false);      

Whenever we receive an event the changeCallback function is called. Before we look at how to handle this event, lets first register the onclick on the canvas element so that we can request a pointer lock.

Register onclick event for the canvas

I just use jQuery for this, since I’m used to it.

        // when element is clicked, we're going to request a
        // pointerlock
        $("#pointerLock").click(function () {
            var canvas = $("#pointerLock").get()[0];
            canvas.requestPointerLock = canvas.requestPointerLock ||
                    canvas.mozRequestPointerLock ||
                    canvas.webkitRequestPointerLock;

            // Ask the browser to lock the pointer)
            canvas.requestPointerLock();
        });

As you can see in this callback we call the requestPointerLock() function, when someone clicks on the canvas.

Handle the callback for the pointerlock

If we click on the canvas, the requestPointerLock() function will be called. This function will fire the ‘pointerlockchange’ event for which we registered the changeCallback function earlier. In this function we do the following:

    // called when the pointer lock has changed. Here we check whether the
    // pointerlock was initiated on the element we want.
    function changeCallback(e) {
        var canvas = $("#pointerLock").get()[0];
        if (document.pointerLockElement === canvas ||
                document.mozPointerLockElement === canvas ||
                document.webkitPointerLockElement === canvas) {

            // we've got a pointerlock for our element, add a mouselistener
            document.addEventListener("mousemove", moveCallback, false);
        } else {

            // pointer lock is no longer active, remove the callback
            document.removeEventListener("mousemove", moveCallback, false);

            // and reset the entry coordinates
            entryCoordinates = {x:-1, y:-1};
        }
    };

We check whether the pointerLockElement, the element that requested the lock, is the one we expect. If this is the case we add a mousemove listener to the canvas element. If this isn’t the case, it implies that the lock is no longer active on our canvas. In that case we remove the mouse listener. In this last case, we also reset a global variable that defines the current coordinates of the spaceship in the canvas (more on that later).

So when we click on the canvas, a pointer lock event is fired, this event is handled by the callback we specified. In the callback we register (or deregister) a mousemove listener, and can start drawing the spaceship and the background whenever we receive a mouse event.

Draw the canvas based on mouse location and movement

In the moveCallback() function we do the following:

    // handles an event on the canvas for the pointerlock example
    var entryCoordinates = {x:-1, y:-1};
    function moveCallback(e) {

        var canvas = $("#pointerLock").get()[0];
        var ctx = canvas.getContext('2d');

        // if we enter this for the first time, get the initial position
        if (entryCoordinates.x == -1) {
            entryCoordinates = getPosition(canvas, e);
        }


        //get a reference to the canvas
        var movementX = e.movementX ||
                e.mozMovementX ||
                e.webkitMovementX ||
                0;

        var movementY = e.movementY ||
                e.mozMovementY ||
                e.webkitMovementY ||
                0;


        // calculate the new coordinates where we should draw the ship
        entryCoordinates.x = entryCoordinates.x + movementX;
        entryCoordinates.y = entryCoordinates.y + movementY;

        if (entryCoordinates.x > $('#pointerLock').width() -65) {
            entryCoordinates.x = $('#pointerLock').width()-65;
        } else if (entryCoordinates.x < 0) {
            entryCoordinates.x = 0;
        }

        if (entryCoordinates.y > $('#pointerLock').height() - 85) {
            entryCoordinates.y = $('#pointerLock').height() - 85;
        } else if (entryCoordinates.y < 0) {
            entryCoordinates.y = 0;
        }


        // determine the direction
        var direction = 0;
        if (movementX > 0) {
            direction = 1;
        } else if (movementX < 0) {
            direction = -1;
        }

        // clear and render the spaceship
        ctx.clearRect(0,0,400,400);
        generateStars(ctx);
        showShip(entryCoordinates.x, entryCoordinates.y, direction,ctx);
    }

This might seem like a lot of javascript. But it shouldn’t be that hard to follow.

If we enter for the first time (entryCoordinates.x == -1) we get the inital position we want to draw on. We use the getPosition() function for this:

    // Returns a position based on a mouseevent on a canvas. Based on code
    // from here: http://miloq.blogspot.nl/2011/05/coordinates-mouse-click-canvas.html
    function getPosition(canvas, event) {
        var x = new Number();
        var y = new Number();

        if (event.x != undefined && event.y != undefined) {
            x = event.x;
            y = event.y;
        }
        else // Firefox method to get the position
        {
            x = event.clientX + document.body.scrollLeft +
                    document.documentElement.scrollLeft;
            y = event.clientY + document.body.scrollTop +
                    document.documentElement.scrollTop;
        }

        x -= canvas.offsetLeft;
        y -= canvas.offsetTop;

        return {x:x, y:y};
    }

Once we have the position, we can use the movementX and movementY properties to determine the relative mouse movement.

        //get a reference to the canvas
        var movementX = e.movementX ||
                e.mozMovementX ||
                e.webkitMovementX ||
                0;

        var movementY = e.movementY ||
                e.mozMovementY ||
                e.webkitMovementY ||
                0;
    }

And based on these properties we determine the position where we need to draw the spaceship:

        // calculate the new coordinates where we should draw the ship
        entryCoordinates.x = entryCoordinates.x + movementX;
        entryCoordinates.y = entryCoordinates.y + movementY;

        if (entryCoordinates.x > $('#pointerLock').width() -65) {
            entryCoordinates.x = $('#pointerLock').width()-65;
        } else if (entryCoordinates.x < 0) {
            entryCoordinates.x = 0;
        }

        if (entryCoordinates.y > $('#pointerLock').height() - 85) {
            entryCoordinates.y = $('#pointerLock').height() - 85;
        } else if (entryCoordinates.y < 0) {
            entryCoordinates.y = 0;
        }

The final step we need to take before we can draw the ship, is to determine the direction the ship is moving in. We’ve got the following spritesheet:

ships2.png

So we can move to the right (direction = 1), move to the left (direction = -1) or have no movement (direction = 0). Based on the relative movementX we determine this value.

        // determine the direction
        var direction = 0;
        if (movementX > 0) {
            direction = 1;
        } else if (movementX < 0) {
            direction = -1;
        }

Now all that is left is drawing the canvas. We first clear the context, and then use the following to generate a random starry background:

    // generate a set of random stars for the canvas.
    function generateStars(ctx) {
        for (var i = 0; i < 50; i++) {

            x = Math.random() * 400;
            y = Math.random() * 400;
            radius = Math.random() * 3;

            ctx.fillStyle = "#FFF";
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
            ctx.closePath();
            ctx.fill();
        }
    }

And draw the ship using this function:

    // Render a ship at the specified position. The direction determines how to render the ship
    // based on a sprite sheet. The sprite was taken frome here:
    // http://atomicrobotdesign.com/blog/web-development/how-to-use-sprite-sheets-with-html5-canvas/
    sprites = new Image();
    sprites.src = 'ships2.png';
    function showShip(ship_x, ship_y, direction, ctx) {

        //    srcX = 83;
        if (direction == -1) {
            srcX = 156;
        } else if (direction == 1) {
            srcX = 83;
        } else if (direction == 0) {
            srcX = 10;
        }


        // 10 is normal  156 is left
        srcY = 0;
        ship_w = 65;
        ship_h = 85;

       ctx.drawImage(sprites, srcX, srcY, ship_w, ship_h, ship_x, ship_y, ship_w, ship_h);
    }

The only thing left to do is handle the unregistration of the pointer lock. When you click ‘esc’ the pointer lock is released and the callback we saw earlier is called:

    function changeCallback(e) {
        var canvas = $("#pointerLock").get()[0];
        if (document.pointerLockElement === canvas ||
                document.mozPointerLockElement === canvas ||
                document.webkitPointerLockElement === canvas) {

            // we've got a pointerlock for our element, add a mouselistener
            document.addEventListener("mousemove", moveCallback, false);
        } else {

            // pointer lock is no longer active, remove the callback
            document.removeEventListener("mousemove", moveCallback, false);

            // and reset the entry coordinates
            entryCoordinates = {x:-1, y:-1};
        }
    };

This time the pointerLockElement won’t point to the canvas, which tells us we can remove the mouselistener. And with all this we’ve got a canvas element that uses pointer lock.

The unlocked variant

In the example you can also see a canvas that doesn’t use pointer lock. The javascript for that is shown here:

    // This function sets up the nopointerlock example. This function creates
    // a simple mousemove listener for the canvas element, and used that information
    // to determine where to draw the ship.
    function setupNoPointerLock() {

        // when we have a canvas without a mouse lock, we need to create a mouse listener
        var canvas2 = $("#noPointerLock").get()[0];
        var context2 = canvas2.getContext('2d');

        var entryCoordinates2 = {x:-1,y:-1};

        canvas2.addEventListener('mousemove', function(evt) {

            if (entryCoordinates2.x == -1) {
                entryCoordinates2 = getPosition(canvas2,evt);
            }

            var newPos = getPosition(canvas2, evt);
            movementX = newPos.x - entryCoordinates2.x;

            // calculate the direction
            var direction = 0;
            if (movementX > 0) {
                direction = 1;
            } else if (movementX < 0) {
                direction = -1;
            }


            // clear and render the spaceship
            context2.clearRect(0,0,400,400);
            generateStars(context2);
            showShip(newPos.x-32, newPos.y-42, direction, context2);
            entryCoordinates2 = newPos;

        });
    }

If you want to look at the sources for this example just go to the example here and use view-source.

Updated: