Nos bastidores do EtchMark: Criando um site que lida com toque, mouse e caneta – e sacudida do dispositivo

O EtchMark é uma nova perspectiva sobre o clássico brinquedo de desenho Etch-A-Sketch, demonstrando o suporte aprimorado do IE11 para toque e padrões emergentes da Web (incluindo Eventos de Ponteiro e Orientação de dispositivo). Nesta postagem, falaremos sobre vários dispositivos que podem ser facilmente adicionados aos seus sites para criar uma experiência suave e natural com toque, mouse, caneta e teclado – e até mesmo responde às sacudidas do dispositivo.

Estrutura da demonstração

O EtchMark permite que você desenhe o que quiser na tela com uso de toque, mouse, caneta ou teclas de seta.  A superfície de desenho é um elemento de tela HTML5 que atualizamos sempre que o botão é girado.  No modo de parâmetro de comparação, usamos a API requestAnimationFrame, que fornece um loop de animação suave de 60 quadros suaves por segundo e bateria mais durável.  As sombras dos botões são criadas com uso de filtros SVG. A aceleração de hardware do IE11 move muito desse trabalho para a GPU, o que leva a uma experiência super-rápida.  Confira o vídeo abaixo para ver esses recursos em ação. Depois, vamos examinar e saber como eles são criados.

O EtchMark usa telas HTML5, requestAnimationFrame, filtros SVG, Eventos de Ponteiro e APIs de Orientação de dispositivos para criar uma nova perspectiva sobre um brinquedo clássico

Eventos de Ponteiro com uso de toque, mouse, teclado e caneta

Os Eventos de ponteiro permitem que você crie experiências que funcionam igualmente bem com mouse, teclado, caneta e toque - tudo isso com codificação correspondente a uma única API. Os Eventos de ponteiro recebem suporte em toda a variedade de dispositivos do Windows e em breve receberão suporte de outros navegadores também.  A especificação Eventos de ponteiro já é uma Recomendação candidata do W3C, e o IE11 dá suporte a uma versão sem prefixo do padrão.

Para começar, a primeira ação necessária é conectar nossos eventos de ponteiro no Knob.js. Primeiro, verificamos a versão padrão sem prefixo e, se essa verificação falhar, voltamos para a versão com prefixo necessária para habilitar o suporte ao IE10.  No exemplo abaixo, hitTarget é uma div que contém a imagem do botão, dimensionada para um tamanho maior de forma que o usuário tenha espaço para usar os dedos facilmente: 

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

    }

Da mesma forma, adicionamos o fallback correto de setPointerCapture para Element.prototype de forma a garantir que também funcione no IE10:

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

A seguir, vamos lidar com o evento pointerDown.  A primeira ação é chamar setPointerCapture em this.hitTarget.  Queremos capturar o ponteiro para que todos os eventos de ponteiro subsequentes sejam resolvidos por este elemento. Isso também garante que outros elementos não disparem eventos, mesmo se o ponteiro for movido para seus limites.  Sem isso, encontraríamos problemas quando os dedos do usuário estivessem na borda da imagem e da div contêiner: a imagem algumas vezes capturaria o evento de ponteiro e outras vezes capturaria a div.  Isso resultaria em uma experiência irregular em que o botão pularia entre as opções. Para resolver isso, capture o ponteiro.

A captura do ponteiro também funciona bem quando o usuário coloca o dedo no botão e o movimenta gradualmente para fora do alvo atingido, sem interromper o giro.  Mesmo se o dedo não for levantado até se mover vários centímetros para fora do alvo atingido, a rotação ainda ocorrerá de forma suave e natural.

A última observação sobre o setPointerCapture é que passamos na propriedade pointerId do evento.  Com isso, podemos dar suporte a vários ponteiros e, assim, o usuário poderá usar um dedo em cada botão simultaneamente sem interferir no evento do outro botão. O suporte para vários botões significa que quando o usuário gira os dois botões ao mesmo tempo, ele obtém um desenho de forma livre em vez de apenas linhas verticais e horizontais.

Também queremos sinalizar dois itens sobre isso, que apontam para nosso objeto do Botão (as sinalizações são feitas por botão):

  • pointerEventInProgress - informa se o ponteiro está pressionado
  • firstContact - informa se o usuário acabou de pressionar com o dedo

    function pointerDown(evt)

    {

        this.hitTarget.setPointerCapture(evt.pointerId);

        this.pointerEventInProgress = true;

        this.firstContact = true;

    }

Por fim, queremos redefinir a sinalização pointerEventInProgress quando o usuário tira o dedo (ou mouse/caneta):

    function pointerUp(evt)

    {

        this.pointerEventInProgress = false;

    }

 

    function pointerCancel(evt)

    {

        this.pointerEventInProgress = false;

    }

O PointerCancel pode ocorrer de duas formas diferentes. A primeira forma é quando o sistema determina que um ponteiro provavelmente não continuará produzindo eventos (por exemplo, devido a um evento de hardware). O evento também dispara se o evento pointerDown já tiver ocorrido. Depois, o ponteiro é usado para manipular o visor da página (por exemplo, com movimentos panorâmicos ou zoom).  Por questões de integridade, é sempre recomendado implementar o pointerUp e o pointerCancel.

Com os eventos para cima, para baixo e cancelar conectados, estamos prontos para implementar o suporte ao pointerMove.  Usamos a sinalização de firstContact para que ele não gire em excesso quando o usuário colocar o dedo pela primeira vez. Depois que o firstContact for apagado, calculamos os deltas de movimento do dedo.  Usamos a trigonometria para transformar nossas coordenadas de início e fim em um ângulo de rotação. Depois, passamos essas informações para a função de desenho:

    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;

        }

    }

Com a implementação de quatro simples manipuladores de evento, agora criamos uma experiência de toque natural e obediente.  Ela dá suporte a vários ponteiros e permite que o usuário manipule os dois botões simultaneamente para produzir um desenho de forma livre.  E a melhor parte: como usamos os Eventos de ponteiro, o mesmo código também funciona para mouse, caneta e teclado.

Colocando mais dedos na brincadeira: adição de suporte a gestos

O código dos Eventos de ponteiros que escrevemos acima funciona muito bem se o usuário girar o botão com uso de um dedo, mas e se ele girar o botão usando dois dedos?  Precisamos usar a trigonometria para calcular o ângulo de rotação, e calcular o ângulo correto com outro dedo se movimentando fica ainda mais complexo.  Em vez de tentar escrever esse código complexo sozinhos, aproveitamos o suporte ao MSGesture do IE11.

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

        });

    }

Com os eventos conectados, já podemos manipular eventos de gestos:

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

        }

    }

Como você pode ver, o MSGesture fornece uma propriedade de rotação simples que representa o ângulo em radianos. Portanto, não precisamos fazer as contas manualmente.  Agora, temos suporte à rotação de dois dedos, que é natural e obediente.

Movimento do dispositivo: adicionando um pouco de movimento

O IE11 dá suporte à DeviceOrientation Event Specification do W3C, que concede acesso às informações de orientação e movimentação física de um dispositivo.  Quando um dispositivo estiver sendo movido ou girado (ou mais precisamente, acelerado), o evento devicemotion é acionado na janela e fornece aceleração (ao mesmo tempo com e sem efeitos de aceleração gravitacional no dispositivo, expressos em metros/segundo2) nos eixos x, y e z . Ele também fornece a taxa de alteração nos ângulos de rotação alfa, beta e gama em graus/segundos.

Nesse caso, queremos apagar a tela sempre que o usuário sacudir o dispositivo.  Para fazer isso, nossa primeira ação é conectar o evento devicemotion (neste caso estamos usando jQuery):

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

Depois, detectamos se o usuário moveu o dispositivo em alguma direção com uma aceleração superior ao nosso valor limite.  Como precisamos detectar a sacudida, temos um contador para garantir a existência de duas sacudidas rápidas seguidas.  Se detectarmos dois movimentos rápidos, apagaremos a tela:

    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;

        }

    }

Para obter mais informações sobre orientação e movimentação de dispositivos, consulte esta postagem no blog do IE.

Bloqueio de orientação

O IE11 também introduz o suporte à API de orientação de tela e recursos como o Bloqueio de orientação.  Como o EtchMark também é um parâmetro de comparação de desempenho, queremos manter o tamanho da nossa tela igual em resoluções de tela diferentes de forma que todos os dispositivos representem o mesmo trabalho para nós.  Isso pode apertar bastante a imagem em telas menores, principalmente no modo retrato.  Para habilitar a melhor experiência, simplesmente bloqueamos a orientação para o modo paisagem:

    window.screen.setOrientationLock("landscape");

Dessa forma, não importa em qual direção o usuário girar o dispositivo, ele sempre visualizará a imagem no modo paisagem.  Também é possível usar screen.unlockOrientation para remover o bloqueio de orientação.

De olho no futuro

Técnicas interoperáveis e baseadas em padrões como os Eventos de ponteiros e eventos de orientação do dispositivo possibilitam novas possibilidades interessantes para seus sites. O suporte ao toque excelente do IE11 fornece uma experiência suave e obediente, além de interatividade. Você pode ir mais além com o IE11 e o MSGesture, fazendo com que cenários como o cálculo de ângulos de rotação de dois dedos se tornem tão simples quanto acessar uma propriedade. Experimente usar essas técnicas no seu próprio site e esperaremos os seus comentários.

Jon Aneja,
gerente de programa, Internet Explorer