Crie com o Chrome

Como levar os blocos LEGO® para a Web multidispositivo

O Build with Chrome, um experimento divertido para usuários do Chrome para computador lançado originalmente na Austrália, foi relançado em 2014 com disponibilidade global, parcerias com o filme O LEGO® MOVIE™ e suporte recém-adicionado para dispositivos móveis. Neste artigo, vamos compartilhar alguns aprendizados do projeto, principalmente em relação à mudança da experiência exclusiva para computadores para uma solução multitela que oferece suporte a entrada por mouse e toque.

A história do Build com o Chrome

A primeira versão do Build with Chrome foi lançada na Austrália em 2012. Queríamos demonstrar o poder da Web de uma forma totalmente nova e levar o Chrome a um público totalmente novo.

O site tinha duas partes principais: o modo "Build", em que os usuários podiam criar usando blocos LEGO, e o modo "Explore" para navegar pelas criações em uma versão LEGO do Google Maps.

O 3D interativo foi essencial para oferecer aos usuários a melhor experiência de construção de LEGO. Em 2012, o WebGL só estava disponível publicamente em navegadores para computador. Por isso, o Build foi criado como uma experiência exclusiva para computadores. O recurso "Explorar" usava o Google Maps para mostrar as criações, mas, ao aproximar o zoom, ele mudava para uma implementação do WebGL do mapa mostrando as criações em 3D, ainda usando o Google Maps como a textura da base. Queríamos criar um ambiente em que os entusiastas da LEGO de todas as idades pudessem expressar a criatividade de forma fácil e intuitiva e conferir as criações uns dos outros.

Em 2013, decidimos expandir o programa "Build with Chrome" para novas tecnologias da Web. Entre essas tecnologias, estava o WebGL no Chrome para Android, que naturalmente permitiria que o Build with Chrome evoluísse para uma experiência móvel. Para começar, desenvolvemos protótipos de toque antes de questionar o hardware da "ferramenta de criação" para entender o comportamento dos gestos e a resposta tátil que podemos encontrar em um navegador em comparação com um app para dispositivos móveis.

Um front-end responsivo

Precisávamos oferecer suporte a dispositivos com entrada por toque e mouse. No entanto, usar a mesma interface em telas sensíveis ao toque pequenas se tornou uma solução subótima devido a restrições de espaço.

No Build, há muita interatividade: zoom in e out, mudança de cores dos tijolos e, claro, seleção, rotação e colocação de tijolos. É uma ferramenta em que os usuários costumam passar muito tempo. Por isso, é importante que eles tenham acesso rápido a tudo o que usam com frequência e se sintam à vontade para interagir com ela.

Ao projetar um aplicativo de toque altamente interativo, você vai descobrir que a tela parece pequena rapidamente e que os dedos do usuário tendem a cobrir grande parte da tela durante a interação. Isso ficou óbvio para nós ao trabalhar com o Builder. Você precisa considerar o tamanho físico da tela, e não os pixels nos gráficos, ao fazer o design. É importante minimizar o número de botões e controles para que o conteúdo real seja dedicado a maior espaço possível na tela.

Nosso objetivo era fazer com que o Build parecesse natural em dispositivos com tela touch, não apenas adicionando a entrada de toque à implementação original para computador, mas fazendo com que ele parecesse realmente destinado ao toque. Acabamos com duas variações da interface: uma para computadores e tablets com telas grandes e outra para dispositivos móveis com telas menores. Sempre que possível, é melhor usar uma única implementação e ter uma transição fluida entre os modos. No nosso caso, determinamos que havia uma diferença significativa na experiência entre esses dois modos, então decidimos usar um ponto de interrupção específico. As duas versões têm muitos recursos em comum, e tentamos fazer a maioria das coisas com apenas uma implementação de código, mas alguns aspectos da IU funcionam de maneira diferente entre as duas.

Usamos dados de user-agent para detectar dispositivos móveis e, em seguida, verificamos o tamanho da viewport para decidir se a interface para dispositivos móveis de tela pequena deve ser usada. É um pouco difícil escolher um ponto de interrupção para o que uma "tela grande" deve ser, porque é difícil conseguir um valor confiável do tamanho físico da tela. Felizmente, no nosso caso, não importa se exibimos a interface de tela pequena em um dispositivo touchscreen com tela grande, porque a ferramenta ainda funciona bem, mas alguns botões podem parecer um pouco grandes demais. No final, definimos o ponto de interrupção como 1.000 pixels. Se você carregar o site de uma janela com mais de 1.000 pixels (no modo paisagem), vai receber a versão de tela grande.

Vamos falar um pouco sobre os dois tamanhos de tela e experiências:

Tela grande, com suporte a mouse e toque

A versão de tela grande é enviada para todos os computadores desktop com suporte a mouse e para dispositivos com tela grande (como o Google Nexus 10). Essa versão é semelhante à solução original para computador em termos de tipos de controles de navegação disponíveis, mas adicionamos suporte a toques e alguns gestos. Ajustamos a interface dependendo do tamanho da janela. Assim, quando um usuário redimensiona a janela, ele pode remover ou redimensionar parte da interface. Para isso, usamos as consultas de mídia do CSS.

Exemplo: quando a altura disponível é menor que 730 pixels, o controle do controle deslizante de zoom no modo Explorar fica oculto:

@media only screen and (max-height: 730px) {
    .zoom-slider {
        display: none;
    }
}

Tela pequena, apenas com suporte ao toque

Essa versão é veiculada em dispositivos móveis e tablets pequenos (dispositivos de destino Nexus 4 e Nexus 7). Essa versão requer suporte a multitoque.

Em dispositivos com tela pequena, precisamos dar ao conteúdo o máximo de espaço possível na tela. Por isso, fizemos algumas mudanças para maximizar o espaço, principalmente removendo elementos usados com pouca frequência:

  • O seletor de blocos de construção é minimizado para um seletor de cores durante a criação.
  • Substituímos os controles de zoom e orientação por gestos multitoque.
  • A funcionalidade de tela cheia do Chrome também é útil para ter mais espaço na tela.
Criar em uma tela grande
Crie em uma tela grande. O seletor de blocos está sempre visível e há alguns controles no lado direito.
Criar em uma tela pequena
Crie em uma tela pequena. O seletor de blocos é minimizado e alguns botões foram removidos.

Desempenho e suporte do WebGL

Os dispositivos touch modernos têm GPUs bastante potentes, mas ainda estão longe de serem tão bons quanto os computadores. Por isso, sabíamos que teríamos alguns desafios com a performance, especialmente no modo de exploração em 3D, em que precisamos renderizar muitas criações ao mesmo tempo.

Do ponto de vista criativo, queríamos adicionar alguns novos tipos de tijolos com formas complexas e até mesmo transparência, recursos que normalmente são muito pesados para a GPU. No entanto, precisávamos ter compatibilidade com versões anteriores e continuar oferecendo suporte às criações da primeira versão. Por isso, não foi possível definir novas restrições, como reduzir significativamente o número total de blocos nas criações.

Na primeira versão do Build, tínhamos um limite máximo de blocos que podiam ser usados em uma criação. Havia um "medidor de tijolos" indicando quantos tijolos restavam. Na nova implementação, alguns dos novos blocos afetam o medidor de blocos mais do que os blocos padrão, reduzindo levemente o número máximo total de blocos. Essa foi uma maneira de incluir novos blocos e manter um desempenho decente.

No modo de visualização 3D, há muitas coisas acontecendo ao mesmo tempo: carregamento de texturas da placa de base, carregamento de criações, animação e renderização de criações e assim por diante. Isso exige muito da GPU e da CPU. Por isso, fizemos muitos perfis de frame nas Ferramentas do desenvolvedor do Chrome para otimizar essas partes o máximo possível. Em dispositivos móveis, decidimos aproximar um pouco mais as criações para não precisar renderizar tantas ao mesmo tempo.

Alguns dispositivos nos fizeram revisitar e simplificar alguns dos shaders do WebGL, mas sempre encontramos uma maneira de resolver e seguir em frente.

Suporte a dispositivos sem WebGL

Queríamos que o site fosse utilizável mesmo que o dispositivo do visitante não oferecesse suporte a WebGL. Às vezes, há maneiras de representar o 3D de forma simplificada usando uma solução de tela ou recursos do CSS3D. Infelizmente, não encontramos uma solução boa o suficiente para replicar os recursos de Build and Explore 3D sem usar o WebGL.

Para manter a consistência, o estilo visual das criações precisa ser o mesmo em todas as plataformas. Poderíamos ter tentado uma solução 2,5D, mas isso teria feito as criações parecerem diferentes de algumas maneiras. Também tivemos que considerar como garantir que as criações feitas com a primeira versão do Build with Chrome tivessem a mesma aparência e funcionassem tão bem na nova versão do site quanto na primeira.

O modo de visualização 2D ainda está disponível para dispositivos que não usam o WebGL, mas não é possível criar novas criações ou fazer a visualização em 3D. Assim, os usuários ainda podem ter uma ideia da profundidade do projeto e do que poderiam criar usando essa ferramenta se estivessem em um dispositivo com suporte a WebGL. O site pode não ser tão valioso para usuários sem suporte ao WebGL, mas pelo menos deve servir como um teaser e fazer com que eles testem o site.

Às vezes, não é possível manter versões substitutas para soluções do WebGL. Há muitos motivos possíveis: desempenho, estilo visual, custos de desenvolvimento e manutenção e assim por diante. No entanto, quando você decidir não implementar um substituto, pelo menos cuide dos visitantes que não têm suporte para WebGL, explique por que eles não podem acessar o site totalmente e dê instruções sobre como resolver o problema usando um navegador compatível com WebGL.

Gerenciamento de ativos

Em 2013, o Google lançou uma nova versão do Google Maps com as mudanças mais significativas na interface desde o lançamento. Por isso, decidimos reformular o Build with Chrome para que ele se encaixe na nova interface do Google Maps e, ao fazer isso, levamos outros fatores em consideração. O novo design é relativamente plano, com cores sólidas e formas simples. Isso nos permitiu usar CSS puro em muitos elementos da interface, minimizando o uso de imagens.

No recurso "Explorar", precisamos carregar muitas imagens: miniaturas das criações, texturas de mapa para as bases e, por fim, as criações em 3D. Tomamos muito cuidado para garantir que não haja vazamento de memória ao carregar novas imagens constantemente.

As criações em 3D são armazenadas em um formato de arquivo personalizado empacotado como uma imagem PNG. Ao manter os dados das criações em 3D armazenados como uma imagem, conseguimos transmitir os dados diretamente para os shaders que renderizam as criações.

Para todas as imagens geradas pelo usuário, o design permitiu que usássemos os mesmos tamanhos de imagem em todas as plataformas, minimizando o uso de armazenamento e largura de banda.

Como gerenciar a orientação da tela

É fácil esquecer o quanto a proporção da tela muda quando você muda do modo retrato para o modo paisagem ou vice-versa. Você precisa considerar isso desde o início ao adaptar para dispositivos móveis.

Em um site tradicional com rolagem ativada, é possível aplicar regras CSS para ter um site responsivo que reorganize conteúdo e menus. Contanto que você possa usar a funcionalidade de rolagem, isso é bastante gerenciável.

Também usamos esse método com o Build, mas tivemos algumas limitações para resolver o layout, porque precisávamos manter o conteúdo sempre visível e ainda ter acesso rápido a vários controles e botões. Para sites de conteúdo puro, como sites de notícias, um layout fluido faz muito sentido, mas para um app de jogo como o nosso, foi uma luta. Foi um desafio encontrar um layout que funcionasse tanto na orientação paisagem quanto na orientação retrato, mantendo uma boa visão geral do conteúdo e uma maneira confortável de interagir. No final, decidimos manter o Build apenas no modo paisagem e pedimos ao usuário para girar o dispositivo.

A ferramenta "Explorar" foi muito mais fácil de resolver nas duas orientações. Só precisamos ajustar o nível de zoom do 3D dependendo da orientação para ter uma experiência consistente.

A maior parte do layout do conteúdo é controlada pelo CSS, mas algumas coisas relacionadas à orientação precisam ser implementadas no JavaScript. Descobrimos que não havia uma boa solução entre dispositivos para usar window.orientation para identificar a orientação. Então, no final, comparamos window.innerWidth e window.innerHeight para identificar a orientação do dispositivo.

if( window.innerWidth > window.innerHeight ){
  //landscape
} else {
  //portrait
}

Como adicionar suporte ao toque

Adicionar suporte a toque ao conteúdo da Web é relativamente simples. A interatividade básica, como o evento de clique, funciona da mesma forma em dispositivos para computador e com recursos de toque, mas, quando se trata de interações mais avançadas, também é necessário processar os eventos de toque: touchstart, touchmove e touchend. Este artigo aborda os princípios básicos de como usar esses eventos. O Internet Explorer não oferece suporte a eventos de toque, mas usa eventos de ponteiro (pointerdown, pointermove, pointerup). Os eventos de ponteiro foram enviados ao W3C para padronização, mas, por enquanto, são implementados apenas no Internet Explorer.

No modo de visualização em 3D, queríamos a mesma navegação da implementação padrão do Google Maps: usar um dedo para mover o mapa e dois dedos para fazer o gesto de pinça e aplicar zoom. Como as criações são em 3D, também adicionamos o gesto de girar com dois dedos. Isso geralmente exige o uso de eventos de toque.

Uma boa prática é evitar computação pesada, como atualizar ou renderizar o 3D nos manipuladores de eventos. Em vez disso, armazene a entrada de toque em uma variável e reaja a ela no loop de renderização requestAnimationFrame. Isso também facilita a implementação de um mouse ao mesmo tempo. Basta armazenar os valores correspondentes do mouse nas mesmas variáveis.

Comece inicializando um objeto para armazenar a entrada e adicionar o listener de evento de início de toque. Em cada manipulador de eventos, chamamos event.preventDefault(). Isso evita que o navegador continue processando o evento de toque, o que pode causar um comportamento inesperado, como rolar ou redimensionar a página inteira.

var input = {dragStartX:0, dragStartY:0, dragX:0, dragY:0, dragDX:0, dragDY:0, dragging:false};
plateContainer.addEventListener('touchstart', onTouchStart);

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
    //start listening to all needed touchevents to implement the dragging
    document.addEventListener('touchmove', onTouchMove);
    document.addEventListener('touchend', onTouchEnd);
    document.addEventListener('touchcancel', onTouchEnd);
  }
}

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }
}

function onTouchEnd(event) {
  event.preventDefault();
  if( event.touches.length === 0){
    handleDragStop();
    //remove all eventlisteners but touchstart to minimize number of eventlisteners
    document.removeEventListener('touchmove', onTouchMove);
    document.removeEventListener('touchend', onTouchEnd);
    //also listen to touchcancel event to avoid unexpected behavior when switching tabs and some other situations
    document.removeEventListener('touchcancel', onTouchEnd);
  }
}

Não estamos fazendo o armazenamento real da entrada nos manipuladores de eventos, mas em manipuladores separados: handleDragStart, handleDragging e handleDragStop. Isso ocorre porque queremos poder chamar esses eventos dos manipuladores de eventos do mouse também. Lembre-se de que, embora improvável, o usuário pode usar o toque e o mouse ao mesmo tempo. Em vez de lidar diretamente com o caso, apenas garantimos que nada exploda.

function handleDragStart(x ,y ){
  input.dragging = true;
  input.dragStartX = input.dragX = x;
  input.dragStartY = input.dragY = y;
}

function handleDragging(x ,y ){
  if(input.dragging) {
    input.dragDX = x - input.dragX;
    input.dragDY = y - input.dragY;
    input.dragX = x;
    input.dragY = y;
  }
}

function handleDragStop(){
  if(input.dragging) {
    input.dragging = false;
    input.dragDX = 0;
    input.dragDY = 0;
  }
}

Ao fazer animações com base em touchmove, muitas vezes é útil armazenar o movimento delta desde o último evento. Por exemplo, usamos isso como um parâmetro para a velocidade da câmera ao se mover por todas as placas de base no recurso "Explorar", já que você não está arrastando as placas de base, mas movendo a câmera.

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );

  //execute animation based on input.dragDX, input.dragDY, input.dragX or input.dragY
 /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

Exemplo incorporado:como arrastar um objeto usando eventos de toque. Implementação semelhante ao arrastar o mapa 3D do recurso Explorar no Build with Chrome: http://cdpn.io/qDxvo

Gestos multitoque

Existem vários frameworks ou bibliotecas, como Hammer ou QuoJS, que podem simplificar o gerenciamento de gestos multitoque. No entanto, se você quiser combinar vários gestos e ter controle total, às vezes é melhor fazer isso do zero.

Para gerenciar os gestos de pinça e rotação, armazenamos a distância e o ângulo entre dois dedos quando o segundo dedo é colocado na tela:

//variables representing the actual scale/rotation of the object we are affecting
var currentScale = 1;
var currentRotation = 0;

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGestureStart(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGestureStart(x1, y1, x2, y2){
  input.isGesture = true;
  //calculate distance and angle between fingers
  var dx = x2 - x1;
  var dy = y2 - y1;
  input.touchStartDistance=Math.sqrt(dx*dx+dy*dy);
  input.touchStartAngle=Math.atan2(dy,dx);
  //we also store the current scale and rotation of the actual object we are affecting. This is needed to support incremental rotation/scaling. We can't assume that an object is always the same scale when gesture starts.
  input.startScale=currentScale;
  input.startAngle=currentRotation;
}

No evento touchmove, medimos continuamente a distância e o ângulo entre esses dois dedos. A diferença entre a distância inicial e a distância atual é usada para definir a escala, e a diferença entre o ângulo inicial e o ângulo atual é usada para definir o ângulo.

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length  === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGesture(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGesture(x1, y1, x2, y2){
  if(input.isGesture){
    //calculate distance and angle between fingers
    var dx = x2 - x1;
    var dy = y2 - y1;
    var touchDistance = Math.sqrt(dx*dx+dy*dy);
    var touchAngle = Math.atan2(dy,dx);
    //calculate the difference between current touch values and the start values
    var scalePixelChange = touchDistance - input.touchStartDistance;
    var angleChange = touchAngle - input.touchStartAngle;
    //calculate how much this should affect the actual object
    currentScale = input.startScale + scalePixelChange*0.01;
    currentRotation = input.startAngle+(angleChange*180/Math.PI);
    //upper and lower limit of scaling
    if(currentScale<0.5) currentScale = 0.5;
    if(currentScale>3) currentScale = 3;
  }
}

Você pode usar a mudança de distância entre cada evento touchmove de maneira semelhante ao exemplo com arrasto, mas essa abordagem geralmente é mais útil quando você quer um movimento contínuo.

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //execute transform based on currentScale and currentRotation
  /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

Você também pode ativar o arrasto do objeto ao fazer os gestos de pinça e girar, se quiser. Nesse caso, você usaria o ponto central entre os dois dedos como a entrada para o gerenciador de arrasto.

Exemplo incorporado:como girar e redimensionar um objeto em 2D. Assim como o mapa no recurso "Explorar" é implementado: http://cdpn.io/izloq

Suporte a mouse e toque no mesmo hardware

Atualmente, há vários laptops, como o Chromebook Pixel, que oferecem suporte a entrada por mouse e toque. Isso pode causar comportamentos inesperados se você não tomar cuidado.

Uma coisa importante é que você não deve detectar apenas o suporte ao toque e ignorar a entrada do mouse, mas oferecer suporte aos dois ao mesmo tempo.

Se você não estiver usando event.preventDefault() nos manipuladores de eventos de toque, alguns eventos de mouse emulados também serão acionados para manter a maioria dos sites não otimizados para toque funcionando. Por exemplo, para um único toque na tela, esses eventos podem ser acionados em uma sequência rápida e nesta ordem:

  1. touchstart
  2. touchmove
  3. touchend
  4. mouseover
  5. mousemove
  6. mousedown
  7. mouseup
  8. clique

Se você tiver interações um pouco mais complexas, esses eventos do mouse podem causar um comportamento inesperado e atrapalhar a implementação. Muitas vezes, é melhor usar event.preventDefault() nos manipuladores de eventos de toque e gerenciar a entrada do mouse em manipuladores de eventos separados. O uso de event.preventDefault() em manipuladores de eventos de toque também impede alguns comportamentos padrão, como rolagem e o evento de clique.

"No Build with Chrome, não queremos que o zoom ocorra quando alguém toca duas vezes no site, embora isso seja padrão na maioria dos navegadores. Portanto, usamos a meta tag da janela de visualização para informar ao navegador que ele não deve fazer zoom quando o usuário tocar duas vezes. Isso também remove o atraso de clique de 300 ms, o que melhora a capacidade de resposta do site. O atraso de clique está presente para diferenciar um toque único de um duplo toque quando o zoom de duplo toque está ativado.

<meta name="viewport" content="width=device-width,user-scalable=no">

Ao usar esse recurso, é sua responsabilidade tornar o site legível em todos os tamanhos de tela, porque o usuário não poderá aumentar o zoom.

Entrada de mouse, toque e teclado

No modo de visualização em 3D, queríamos que houvesse três maneiras de navegar pelo mapa: mouse (arrastar), toque (arrastar, usar o gesto de pinça para aplicar zoom e girar) e teclado (navegar com as teclas de seta). Todos esses métodos de navegação funcionam de maneira um pouco diferente, mas usamos a mesma abordagem em todos eles: definir variáveis em gerenciadores de eventos e agir sobre isso no loop requestAnimationFrame. O loop requestAnimationFrame não precisa saber qual método é usado para navegar.

Por exemplo, podemos definir o movimento do mapa (dragDX e dragDY) com os três métodos de entrada. Confira a implementação do teclado:

document.addEventListener('keydown', onKeyDown );
document.addEventListener('keyup', onKeyUp );

function onKeyDown( event ) {
  input.keyCodes[ "k" + event.keyCode ] = true;
  input.shiftKey = event.shiftKey;
}

function onKeyUp( event ) {
  input.keyCodes[ "k" + event.keyCode ] = false;
  input.shiftKey = event.shiftKey;
}

//this needs to be called every frame before animation is executed
function handleKeyInput(){
  if(input.keyCodes.k37){
    input.dragDX = -5; //37 arrow left
  } else if(input.keyCodes.k39){
    input.dragDX = 5; //39 arrow right
  }
  if(input.keyCodes.k38){
    input.dragDY = -5; //38 arrow up
  } else if(input.keyCodes.k40){
    input.dragDY = 5; //40 arrow down
  }
}

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //because keydown events are not fired every frame we need to process the keyboard state first
  handleKeyInput();
  //implement animations based on what is stored in input
   /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX = 0;
  input.dragDY = 0;
}

Exemplo incorporado:navegação com mouse, toque e teclado: http://cdpn.io/catlf

Resumo

Adaptar o Build com o Chrome para oferecer suporte a dispositivos com tela touch com muitos tamanhos de tela diferentes foi uma experiência de aprendizado. A equipe não tinha muita experiência nesse nível de interatividade em dispositivos com tela touch e aprendemos muito ao longo do caminho.

O maior desafio foi resolver a experiência do usuário e o design. Os desafios técnicos foram gerenciar muitos tamanhos de tela, eventos de toque e problemas de desempenho.

Embora houvesse alguns desafios com os shaders do WebGL em dispositivos com tela touchscreen, isso funcionou quase melhor do que o esperado. Os dispositivos estão ficando cada vez mais poderosos, e as implementações do WebGL estão melhorando rapidamente. Acreditamos que vamos usar muito mais a WebGL em dispositivos no futuro próximo.

Agora, se ainda não fez isso, crie algo incrível.