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