Статьи

HTML5: используйте API блокировки указателя, чтобы ограничить движение мыши элементом

В этой статье мы подробнее рассмотрим один из новых веб-API, недавно появившихся в Chrome: API блокировки указателя ( http://www.w3.org/TR/pointerlock/ ). С помощью этого набора API можно заблокировать мышь на конкретном элементе HTML. С заблокированной мышью вы можете перемещать мышь, и она никогда не покинет фокус элемента. Отлично подходит для игр, 3D-представлений и, вероятно, многих других вещей. Определение с сайта Mozilla прекрасно объясняет этот API:

«Блокировка указателя (ранее называемая блокировкой мыши) предоставляет методы ввода, основанные на перемещении мыши во времени (т. Е. Дельтах), а не только на абсолютной позиции курсора мыши. Она дает вам доступ к необработанному движению мыши, блокирует цель события мыши для одного элемента, устраняет ограничения на то, как далеко может двигаться мышь в одном направлении, и удаляет курсор из поля зрения ».

Примечание для тех из вас, кто использует Firefox. Firefox имеет реализацию API блокировки Pointer. Но когда вы попробуете примеры на этой странице, вы заметите, что это не сработает. Причина в том, что на момент написания API блокировки указателя в Firefox был привязан к полноэкранному API. Таким образом, чтобы блокировка указателя работала в Firefox, элемент, на который вы хотите заблокировать указатель, должен быть в полноэкранном режиме. Как только это требование будет снято, пример на этой странице также будет работать на Firefox.

Демо время

Чтобы продемонстрировать этот API, я создал простой пример:

PointerLock v. Non-PointerLock.png
(нажмите на изображение, чтобы запустить демо)

В этом примере показаны два элемента canvas. Первый элемент холста, когда вы наводите на него мышью, показывает звездный космический корабль. С помощью CSS курсор скрыт, а космический корабль следует за вашей мышью. Мы моделируем движение, используя лист спрайта, чтобы показать разные корабли. Что вы заметите, так это то, что когда вы перемещаете мышь далеко, элемент теряет фокус, и корабль перестает двигаться, а звезды перестают обновляться: мышь не привязана к элементу.

Когда вы нажимаете на второй элемент, предполагая, что вы используете Chrome (кстати, я использую версию 24.0.1284.2 dev), вы увидите всплывающее окно, показанное на предыдущем скриншоте.

Ваш указатель мыши теперь привязан к элементу, и вы можете перемещать мышь вокруг, сколько хотите, и он останется заблокированным на холсте, обновляя космический корабль и звездный фон. Нажмите «esc», чтобы освободить указатель.

Я проработаю все шаги, которые вам необходимо предпринять, чтобы воссоздать эту демонстрацию:

  • Добавить прослушиватель событий для события pointerlockchange
  • Зарегистрируйте щелчок на элементе canvas
  • Обрабатывать обратный вызов, когда указатель заблокирован
  • Нарисуйте холст на основе расположения и движения мыши
  • Обрабатывать обратный вызов, когда указатель разблокирован

Добавить прослушиватель событий для события pointerlockchange

Первое, что мы собираемся сделать, это зарегистрировать обработчик обратного вызова для события изменения указателя. Поскольку этот API все еще находится на ранних стадиях, мы должны ставить префиксы перед вызовами ‘moz’ или ‘webkit’. Для регистрации обратного вызова мы используем следующий код:

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

Всякий раз, когда мы получаем событие, вызывается функция changeCallback. Прежде чем мы рассмотрим, как обрабатывать это событие, давайте сначала зарегистрируем onclick на элементе canvas, чтобы мы могли запросить блокировку указателя.

Зарегистрируйте событие onclick для холста

Я просто использую для этого jQuery, так как я к этому привык.

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

Как вы можете видеть в этом обратном вызове, мы вызываем функцию requestPointerLock (), когда кто-то нажимает на холст.

Обработка обратного вызова для блокировки указателя

Если мы нажмем на холст, будет вызвана функция requestPointerLock (). Эта функция будет запускать событие pointerlockchange, для которого мы ранее зарегистрировали функцию changeCallback. В этой функции мы делаем следующее:

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

Мы проверяем, является ли указатель, элемент, который запросил блокировку, тем элементом, который мы ожидаем. Если это так, мы добавляем слушатель mousemove к элементу canvas. Если это не так, это означает, что блокировка больше не активна на нашем холсте. В этом случае мы удаляем слушателя мыши. В этом последнем случае мы также сбрасываем глобальную переменную, которая определяет текущие координаты космического корабля на холсте (подробнее об этом позже).

Поэтому, когда мы нажимаем на холст, происходит событие блокировки указателя, это событие обрабатывается с помощью указанного нами обратного вызова. В обратном вызове мы регистрируем (или отменяем регистрацию) слушателя перемещения мыши и можем начать рисовать космический корабль и фон всякий раз, когда мы получаем событие мыши.

Нарисуйте холст на основе расположения и движения мыши

В функции moveCallback () мы делаем следующее:

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

Это может показаться большим количеством JavaScript. Но не должно быть так сложно следовать.

Если мы входим в первый раз (entryCoordinates.x == -1), мы получаем начальную позицию, на которой хотим рисовать. Мы используем функцию getPosition () для этого:

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

Как только у нас есть позиция, мы можем использовать свойства MoveX и MoveY, чтобы определить относительное движение мыши.

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

Последний шаг, который нам нужно сделать, прежде чем мы начнем рисовать корабль, — это определить направление движения корабля. У нас есть следующая таблица спрайтов:

ships2.png

Таким образом, мы можем двигаться вправо (направление = 1), двигаться влево (направление = -1) или не двигаться (направление = 0). На основании относительного движения X мы определяем это значение.

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

Теперь осталось только нарисовать холст. Сначала мы очищаем контекст, а затем используем следующее для создания случайного звездного фона:

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

И нарисуйте корабль, используя эту функцию:

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

Единственное, что осталось сделать, это обработать отмену регистрации блокировки указателя. Когда вы нажимаете «esc», блокировка указателя снимается, и вызывается обратный вызов, который мы видели ранее:

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

На этот раз pointerLockElement не будет указывать на холст, который говорит нам, что мы можем удалить mouselistener. И со всем этим у нас есть элемент canvas, который использует блокировку указателя.

Разблокированный вариант

В примере вы также можете увидеть холст, который не использует блокировку указателя. Javascript для этого показан здесь:

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

Если вы хотите посмотреть на источники для этого примера, просто перейдите к примеру здесь и используйте view-source.