EtchMark — взгляд изнутри: создание веб-сайта с поддержкой управления с помощью касаний, мыши, пера, а также посредством встряхивания устройства

EtchMark — новый вариант классической игрушки для рисования, демонстрирующий улучшенную поддержку сенсорного ввода и развивающихся веб-стандартов (включая Pointer Events и Device Orientation) в Internet Explorer 11. В этой записи мы рассмотрим несколько компонентов, которые можно легко добавить на ваши сайты, чтобы создать плавно и естественно работающий интерфейс с поддержкой сенсорного ввода, мыши, пера и клавиатуры, реагирующий даже на встряхивание устройства.

Структура демонстрации

EtchMark позволяет рисовать на экране все, что угодно, с помощью касаний, мыши, пера или клавиш со стрелками.  Поверхность для рисования представляет собой HTML5-элемент canvas, обновляемый при каждом повороте рукоятки.  В режиме теста мы используем API-интерфейс requestAnimationFrame, обеспечивающий плавный цикл анимации с частотой 60 кадров в секунду и более длительное время работы батареи.  Тени, отбрасываемые рукоятками, созданы с использованием фильтров SVG. Благодаря поддержке аппаратного ускорения в Internet Explorer 11 большая часть вычислительной нагрузки переносится на GPU, что проявляется в очень быстрой работе.  Посмотрите следующее видео, чтобы увидеть эти компоненты в действии, а затем мы подробно рассмотрим, как все устроено.

Для создания нового варианта классической игрушки в EtchMark используются HTML5-элемент canvas, requestAnimationFrame, фильтры SVG, API-интерфейсы Pointer Events и Device Orientation

Управление с помощью касаний, мыши, клавиатуры и пера с использованием событий указателя

События указателя позволяют создавать интерфейсы, с которыми одинаково удобно работать с помощью мыши, клавиатуры, пера и касаний, — все программируется с использованием одного API. События указателя поддерживаются всем спектром устройств с Windows и скоро будут поддерживаться и другими браузерами.  Спецификация Pointer Events получила статус Candidate Recommendation консорциума W3C, и Internet Explorer 11 поддерживает беспрефиксную версию этого стандарта.

Для начала нам в первую очередь необходимо подключить события указателя в файле Knob.js. Сначала мы проверяем стандартную беспрефиксную версию, и если эта проверка завершается со сбоем, переходим к версии с префиксом, необходимой для поддержки Internet Explorer 10.  В следующем примере hitTarget представляет собой простой элемент div, содержащий изображение рукоятки слегка увеличенного размера, чтобы пользователю было удобно управлять с помощью пальцев: 

    if (navigator.pointerEnabled)

    {

        this.hitTarget.addEventListener("pointerdown", pointerDown.bind(this));

        this.hitTarget.addEventListener("pointerup", pointerUp.bind(this));

        this.hitTarget.addEventListener("pointercancel", pointerCancel.bind(this));

        this.hitTarget.addEventListener("pointermove", pointerMove.bind(this));

    }

    else if (navigator.msPointerEnabled)

    {

        this.hitTarget.addEventListener("MSPointerDown", pointerDown.bind(this));

        this.hitTarget.addEventListener("MSPointerUp", pointerUp.bind(this));

        this.hitTarget.addEventListener("MSPointerCancel", pointerCancel.bind(this));

        this.hitTarget.addEventListener("MSPointerMove", pointerMove.bind(this));

    }

Аналогичным образом мы добавляем правильный запасной вариант для setPointerCapture к Element.prototype, чтобы все работало и в Internet Explorer 10:

    Element.prototype.setPointerCapture = Element.prototype.setPointerCapture || Element.prototype.msSetPointerCapture;

Затем давайте обработаем событие pointerDown.  В первую очередь мы вызываем setPointerCapture для this.hitTarget.  Мы хотим захватить указатель, чтобы все последующие события указателя обрабатывались этим элементом. Это также гарантирует, что все остальные элементы не будут генерировать события, даже если указатель переместится к их границам.  Без этого мы столкнулись бы с проблемами, когда палец пользователя находится на краю изображения и элемента div: в какие-то моменты событие указателя получало бы изображение, а в другие моменты — div.  Это привело бы к нечеткой работе и скачкам рукоятки. Захват указателя позволяет легко этого избежать.

Кроме того, захват указателя прекрасно работает, когда пользователь помещает палец на рукоятку, а затем постепенно перемещает его с целевого объекта, продолжая вращение.  Даже если палец не отрывается от экрана, пока не переместился на несколько дюймов в сторону от целевого объекта, вращение выполняется плавно и естественно.

Последнее, о чем следует упомянуть в связи с setPointerCapture, — мы передаем в него свойство pointerId события.  Это позволяет поддерживать несколько указателей. То есть пользователь может одновременно использовать обе рукоятки, и события одной рукоятки не мешают событиям другой. Поддержка нескольких рукояток означает, что, вращая обе рукоятки одновременно, пользователь может рисовать произвольные линии, а не только вертикальные и горизонтальные.

Мы хотим также установить два флага на this, указывающие на наш объект Knob (по флагу для каждой рукоятки):

  • pointerEventInProgress — сообщает нам, "нажат" ли указатель
  • firstContact — сообщает нам, что пользователь только что приложил палец

    function pointerDown(evt)

    {

        this.hitTarget.setPointerCapture(evt.pointerId);

        this.pointerEventInProgress = true;

        this.firstContact = true;

    }

Наконец, мы хотим сбросить флаг pointerEventInProgress, когда пользователь поднимает палец (или мышь, или перо):

    function pointerUp(evt)

    {

        this.pointerEventInProgress = false;

    }

 

    function pointerCancel(evt)

    {

        this.pointerEventInProgress = false;

    }

PointerCancel может происходить двумя разными способами. Во-первых, когда система определила, что указатель, вероятно, больше не будет создавать события (например, из-за события оборудования). Это событие также генерируется, если уже произошло событие pointerDown, а затем указатель используется для управления окном просмотра страницы (например при панорамировании или масштабировании).  Для полноты всегда рекомендуется реализовать и pointerUp, и pointerCancel.

Когда подключены события pointerUp, pointerDown и pointerCancel, мы готовы реализовать поддержку pointerMove.  Мы используем флаг firstContact, чтобы не перекрутить, когда пользователь впервые касается элемента. После очистки флага firstContact, мы просто вычисляем приращения перемещения пальца.  Для преобразования начальных и конечных координат в угол поворота, который мы затем передаем в функцию рисования, используется тригонометрия:

    function pointerMove(evt)

    {

        //centerX and centerY are the centers of the hit target (div containing the knob)

        evt.x -= this.centerX;

        evt.y -= this.centerY;

 

        if (this.pointerEventInProgress)

        {

            //Trigonometry calculations to figure out rotation angle

 

            var startXDiff = this.pointerEventInitialX - this.centerX;

            var startYDiff = this.pointerEventInitialY - this.centerY;

 

            var endXDiff = evt.x - this.centerX;

            var endYDiff = evt.y - this.centerY;

 

            var s1 = startYDiff / startXDiff;

            var s2 = endYDiff / endXDiff;

 

            var smoothnessFactor = 2;

            var rotationAngle = -Math.atan((s1 - s2) / (1 + s1 * s2)) / smoothnessFactor;

 

            if (!isNaN(rotationAngle) && rotationAngle !== 0 && !this.firstContact)

            {

                //it’s a real rotation value, so rotate the knob and draw to the screen

                this.doRotate({ rotation: rotationAngle, nonGesture: true });

            }

 

            //current x and y values become initial x and y values for the next event

            this.pointerEventInitialX = evt.x;

            this.pointerEventInitialY = evt.y;

            this.firstContact = false;

        }

    }

Реализовав четыре простых обработчика событий, мы создали возможности управления касаниями, кажущиеся очень естественными и "слушающиеся" ваших пальцев.  Благодаря поддержке нескольких указателей пользователь может управлять обеими рукоятками одновременно для рисования произвольных линий.  А главное — поскольку мы использовали Pointer Events, тот же код подходит и для мыши, пера и клавиатуры.

Больше пальцев участвуют в игре: добавление поддержки жестов

Код Pointer Events, написанный нами выше, прекрасно работает, если пользователь вращает рукоятку одним пальцем. А что если он использует для вращения два пальца?  Нам пришлось использовать тригонометрию для вычисления угла поворота, а расчет правильного угла с учетом второго движущегося пальца становится еще более сложной задачей.  Вместо самостоятельного написания такого сложного кода мы используем преимущества поддержки MSGesture в Internet Explorer 11.

    if (window.MSGesture)

    {

        var gesture = new MSGesture();

        gesture.target = this.hitTarget;

 

        this.hitTarget.addEventListener("MSGestureChange", handleGesture.bind(this));

        this.hitTarget.addEventListener("MSPointerDown", function (evt)

        {

            // adds the current mouse, pen, or touch contact for gesture recognition

            gesture.addPointer(evt.pointerId);

        });

    }

Когда эти события подключены, мы можем обрабатывать события жестов:

    function handleGesture(evt)

    {

        if (evt.rotation !== 0)

        {

            //evt.nonGesture is a flag we defined in the pointerMove method above.

            //It will be true when we’re handling a pointer event, and false when

            //we’re handling an MSGestureChange event

            if (!evt.nonGesture)

            {

                //set to false if we got here via Gesture so flag is in correct state

                this.pointerEventInProgress = false;

            }

 

            var angleInDegrees = evt.rotation * 180 / Math.PI;

 

            //rotate the knob visually

            this.rotate(angleInDegrees);

 

            //draw based on how much we rotated

            this.imageSketcher.draw(this.elementName, angleInDegrees);

        }

    }

Как видите, MSGesture дает нам простое свойство rotation, представляющее угол в радианах, поэтому нам не приходится выполнять все математические вычисления вручную.  Благодаря этому мы получаем поддержку вращения двумя пальцами, которое кажется очень естественным и "слушается" малейшего прикосновения.

Перемещение устройства: добавление небольшого встряхивания

Internet Explorer 11 поддерживает спецификацию DeviceOrientation Event консорциума W3C, позволяющую получать доступ к информации о физической ориентации и перемещении устройства.  Когда устройство перемещается или поворачивается (точнее, испытывает ускорение), для окна генерируется событие devicemotion, предоставляющее значения ускорения (с эффектом гравитационного ускорения и без него, выраженные в метрах в секунду в квадрате) по осям x, y и z.  Оно также предоставляет скорость изменения углов поворота альфа, бета и гамма в градусах в секунду.

В данном случае мы хотим очищать экран в любой момент, когда пользователь встряхивает устройство.  Для этого мы в первую очередь подключаем событие devicemotion (в данном случае мы используем jQuery):

    $(window).on("devicemotion", detectShaking);

Затем мы проверяем, переместил ли пользователь устройство в каком-либо направлении с ускорением, превышающим пороговое значение.  Поскольку нам требуется обнаружить встряхивание, у нас есть счетчик, позволяющий убедиться в том, что произошло два таких быстрых перемещения подряд.  Если мы обнаружили два быстрых перемещения, мы стираем изображение с экрана:

    var nAccelerationsInARow = 0;

 

    var detectShaking = function (evt)

    {

        var accl = evt.originalEvent.acceleration;

 

        var threshold = 6;

        if (accl.x > threshold || accl.y > threshold || accl.z > threshold)

        {

            nAccelerationsInARow++;

            if (nAccelerationsInARow > 1)

            {

                eraseScreen();

                nAccelerationsInARow = 0;

            }

        }

        else

        {

            nAccelerationsInARow = 0;

        }

    }

Дополнительную информацию об ориентации и перемещении устройства можно прочитать в этой записи блога по Internet Explorer.

Блокировка ориентации

В Internet Explorer 11 также реализована поддержка API-интерфейса Screen Orientation и таких компонентов, как Orientation Lock.  Поскольку EtchMark является также тестом производительности, мы хотим поддерживать одинаковый размер элемента canvas для разных разрешений экрана, чтобы выполнять одинаковый объем работы на каждом устройстве.  Это приводит к тому, что на экранах меньшего размера элементы могут располагаться довольно тесно, особенно при книжной ориентации.  Чтобы обеспечить наилучший опыт работы, мы просто фиксируем альбомную ориентацию:

    window.screen.setOrientationLock("landscape");

Таким образом, независимо от того, в какую сторону пользователь поворачивает устройство, он всегда видит изображение в режиме с альбомной ориентацией.  Вы можете также использовать screen.unlockOrientation для снятия блокировки ориентации.

Заглядывая вперед

Основанные на стандартах и обеспечивающие высокую совместимость способы, такие как использование событий Pointer Events и Device Orientation, открывают новые удивительные возможности для ваших веб-сайтов. Прекрасная поддержка сенсорного ввода в Internet Explorer 11 обеспечивает плавное управление и интерактивность, когда веб-страницы "слушаются" малейшего прикосновения ваших пальцев. С помощью Internet Explorer 11 и MSGesture вы можете пойти еще дальше и сделать такие сценарии, как вычисление углов поворота двумя пальцами, такими же простыми, как доступ к свойству. Попробуйте использовать эти способы на своем сайте, мы с нетерпением ждем ваших отзывов.

Йон Анейя (Jon Aneja)
Руководитель программы, Internet Explorer