O front-end da Terra-média

Tutorial do desenvolvimento multidispositivo

Em nosso primeiro artigo sobre o desenvolvimento do experimento do Google Chrome A Journey Through Middle-earth, focamos no desenvolvimento em WebGL para dispositivos móveis. Neste artigo, discutimos os desafios, problemas e soluções que encontramos na criação do restante do front-end HTML5.

Três versões do mesmo site

Vamos começar falando um pouco sobre como adaptar esse experimento para funcionar em computadores desktop e dispositivos móveis do ponto de vista do tamanho da tela e da capacidade dos dispositivos.

Todo o projeto é baseado em um estilo muito “cinemático”, em que queríamos manter a experiência dentro de um quadro fixo orientado a paisagem para manter a magia do filme. Como uma grande parte do projeto é composta por minijogos interativos, também não faria sentido permitir que eles ultrapassassem o frame.

Podemos tomar a página de destino como um exemplo de como adaptamos o design para diferentes tamanhos.

As águias nos deixaram na página de destino.
Os águias nos deixaram na página de destino.

O site tem três modos diferentes: computador, tablet e smartphone. Não apenas para lidar com o layout, mas porque precisamos lidar com recursos carregados durante a execução e adicionar várias otimizações de desempenho. Com dispositivos com resolução superior a computadores desktop e laptops, mas com desempenho pior do que os telefones, definir o conjunto final de regras não é uma tarefa fácil.

Estamos usando dados do user agent para detectar dispositivos móveis e um teste do tamanho da janela de visualização para segmentar tablets nesses dispositivos (645 pixels ou mais). Cada modo diferente pode, de fato, renderizar todas as resoluções, porque o layout é baseado em consultas de mídia ou posicionamento relativo/porcentagem com JavaScript.

Como, nesse caso, os designs não são baseados em grades ou regras e são bastante exclusivos entre as diferentes seções, depende do elemento e do cenário específicos em relação a quais pontos de interrupção ou estilos usar. Aconteceu mais de uma vez que configuramos o layout perfeito com belos sass-mixins e consultas de mídia, e depois precisávamos adicionar um efeito com base na posição do mouse ou em objetos dinâmicos, e acabamos reescrevendo tudo em JavaScript.

Também adicionamos uma classe com o modo atual na tag head para que possamos usar essas informações nos nossos estilos, como neste exemplo (em SCSS):

.loc-hobbit-logo {

  // Default values here.

  .desktop & {
     // Applies only in desktop mode.
  }

 .tablet &, .mobile & {
   
   // Different asset for mobile and tablets perhaps.

   @media screen and (max-height: 760px), (max-width: 760px) {
     // Breakpoint-specific styles.
   }

   @media screen and (max-height: 570px), (max-width: 400px) {
     // Breakpoint-specific styles.
   }
 }
}

Oferecemos suporte a todos os tamanhos, até 360 x 320, o que é bastante desafiador na hora de criar uma experiência imersiva na Web. No computador, temos um tamanho mínimo antes de mostrarmos as barras de rolagem porque queremos que você use o site em uma janela de visualização maior, se possível. Em dispositivos móveis, decidimos permitir os modos paisagem e retrato até as experiências interativas, em que pedimos para você colocar o dispositivo no modo paisagem. O argumento contra isso é que ele não é tão imersivo em retrato quanto em paisagem; mas o site foi dimensionado muito bem, então nós o mantivemos.

É importante observar que o layout não deve ser confundido com a detecção de recursos, como tipo de entrada, orientação do dispositivo, sensores etc. Esses recursos podem existir em todos esses modos e devem abranger todos. A compatibilidade com mouse e toque ao mesmo tempo é um exemplo. Compensação de Retina pela qualidade, mas a maior parte do desempenho é outra; às vezes, uma qualidade menor é melhor. Por exemplo, a tela tem metade da resolução nas experiências WebGL em telas de retina, que, de outra forma, teriam que renderizar quatro vezes o número de pixels.

Usamos com frequência a ferramenta de emulador no DevTools durante o desenvolvimento, principalmente no Chrome Canary, que tem novos recursos aprimorados e muitas predefinições. É uma boa maneira de validar rapidamente o design. Ainda precisávamos testar regularmente em dispositivos reais. Um dos motivos era porque o site está se adaptando para tela cheia. Na maioria dos casos, as páginas com rolagem vertical ocultam a interface do navegador (no momento, o Safari no iOS7 tem problemas), mas tivemos que ajustar tudo, independentemente disso. Também usamos uma predefinição no emulador e mudamos a configuração de tamanho da tela para simular a perda de espaço disponível. Testar em dispositivos reais também é importante para monitorar o consumo de memória e o desempenho.

Como processar o estado

Depois da página de destino, vamos acessar o mapa da Terra-média. Você percebeu que o URL mudou? O site é um aplicativo de página única que usa a API History para gerenciar o roteamento.

Cada seção do site é seu próprio objeto, que herda um modelo de funcionalidade, como elementos DOM, transições, carregamento de ativos, descarte, entre outros. Quando você explora diferentes partes do site, as seções são iniciadas, os elementos são adicionados e removidos do DOM e os recursos da seção atual são carregados.

Como o usuário pode pressionar o botão "Voltar" do navegador ou navegar pelo menu a qualquer momento, tudo o que for criado precisa ser descartado em algum momento. As animações e os tempos limite precisam ser interrompidos e descartados. Caso contrário, eles vão causar comportamentos indesejados, erros e vazamentos de memória. Essa nem sempre é uma tarefa fácil, especialmente quando os prazos estão se aproximando e você precisa concluir tudo o mais rápido possível.

Mostrar os locais

Para mostrar as belas configurações e os personagens da Terra-média, construímos um sistema modular de componentes de imagem e texto que você pode arrastar ou deslizar horizontalmente. Não ativamos uma barra de rolagem aqui, já que queremos ter velocidades diferentes em intervalos distintos, como em sequências de imagem em que você interrompe o movimento lateralmente até que o clipe termine.

Salão de Thranduil
Cronograma do Thranduil's Hall

Linha do tempo

Quando o desenvolvimento começou, não sabíamos o conteúdo dos módulos para cada local. O que sabíamos era que queríamos uma forma baseada em modelo de mostrar diferentes tipos de mídia e informações em uma linha do tempo horizontal que nos desse a liberdade de ter seis apresentações locais diferentes sem ter que recriar tudo seis vezes. Para gerenciar isso, criamos um controlador de linha do tempo que lida com a movimentação dos módulos com base nas configurações e nos comportamentos dos módulos.

Módulos e componentes de comportamento

Os diferentes módulos compatíveis são: sequência de imagens, imagem estática, cena paralaxe, cena de deslocamento de foco e texto.

O módulo de cena paralaxe tem um plano de fundo opaco com um número personalizado de camadas que detecta o progresso da janela de visualização para posições exatas.

A cena de mudança de foco é uma variante do bucket de paralaxe, com a adição de que usamos duas imagens para cada camada, que aparecem e desaparecem para simular uma mudança de foco. Tentamos usar o filtro de desfoque, mas ele ainda é caro, então vamos esperar pelos sombreadores CSS.

O conteúdo no módulo de texto é compatível com o recurso de arrastar com o plug-in TweenMax Draggable. Também é possível usar a roda de rolagem ou deslizar com dois dedos para rolar verticalmente. Observe o throw-props-plugin, que adiciona uma física no estilo fling ao deslizar e soltar.

Os módulos também podem ter diferentes comportamentos adicionados como um conjunto de componentes. Todos eles têm os próprios seletores e configurações de segmentação. Translação para mover um elemento, dimensionar para aplicar zoom, pontos de acesso para sobreposição de informações, depurar métricas para testar visualmente, uma sobreposição de título inicial, uma camada de iluminação e muito mais. Eles serão anexados ao DOM ou controlando o elemento de destino dentro do módulo.

Com isso em vigor, podemos criar os diferentes locais apenas com um arquivo de configuração que define quais recursos serão carregados e configurar os diferentes tipos de módulos e componentes.

Sequências de imagens

Os módulos mais desafiadores em relação ao desempenho e ao tamanho de download são a sequência da imagem. Há muita coisa para ler sobre esse assunto. Em celulares e tablets, substituímos isso por uma imagem estática. Precisamos de muitos dados para decodificar e armazenar na memória se quisermos uma qualidade decente em dispositivos móveis. Tentamos várias soluções alternativas, usando primeiro uma imagem de plano de fundo e uma Folha de Sprite, mas isso causou problemas de memória e atraso quando a GPU precisava alternar entre as folhas de sprite. Depois, tentamos trocar elementos img, mas a operação foi muito lenta. Desenhar um frame de uma Folha de Sprite para uma Tela era a melhor opção, então começamos a otimizá-lo. Para economizar tempo de computação em cada frame, os dados da imagem a serem gravados na tela são pré-processados por uma tela temporária e salvos com putImageData() em uma matriz, decodificados e prontos para uso. A Folha de Sprite original pode ser coletada como lixo e armazenamos apenas a quantidade mínima de dados necessários na memória. Pode ser menos para armazenar imagens não decodificadas, mas conseguimos um desempenho melhor ao refinar a sequência dessa maneira. Os frames são bem pequenos, somente 640 x 400, mas só serão visíveis quando você acessar o conteúdo. Quando você para, uma imagem em alta resolução é carregada e aparece rapidamente.

var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;

var ctx = canvas.getContext('2d');
ctx.drawImage(sheet, 0, 0);

var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;

var canvasPaste = canvas.cloneNode(false);
canvasPaste.width = tileWidth;
canvasPaste.height = tileHeight;

var i, j, canvasPasteTemp, imgData, 
var currentIndex = 0;
var startIndex = index * 16;
for (i = 0; i < tilesY; i++) {
  for (j = 0; j < tilesX; j++) {
    // Store the image data of each tile in the array.
    canvasPasteTemp = canvasPaste.cloneNode(false);
    imgData = ctx.getImageData(j * tileWidth, i * tileHeight, tileWidth, tileHeight);
    canvasPasteTemp.getContext('2d').putImageData(imgData, 0, 0);

    list[ startIndex + currentIndex ] = imgData;

    currentIndex++;
  }
}

As folhas de sprite são geradas com o Imagemagick. Este é um exemplo simples no GitHub que mostra como criar uma Folha de Sprite de todas as imagens dentro de uma pasta.

Como animar os módulos

Para colocar os módulos na linha do tempo, uma representação oculta da linha do tempo, exibida fora da tela, controla o "marcador" e a largura da linha do tempo. Isso podia ser feito apenas com código, mas era bom com uma representação visual no desenvolvimento e na depuração. Em anúncios reais, o recurso é atualizado apenas no redimensionamento para definir as dimensões. Alguns módulos preenchem a janela de visualização e outros têm sua própria proporção. Por isso, foi um pouco complicado dimensionar e posicionar tudo em todas as resoluções para que tudo ficasse visível e não muito cortado. Cada módulo tem dois indicadores de progresso, um para a posição visível na tela e outro para a duração do próprio módulo. Ao fazer movimento de paralaxe, muitas vezes é difícil calcular as posições inicial e final dos objetos para sincronizar com a posição esperada quando eles estão em visualização. É bom saber exatamente quando um módulo entra na visualização, reproduz a linha do tempo interna e quando sai da visualização novamente.

Cada módulo tem uma camada preta sutil na parte superior que ajusta a opacidade para que fique totalmente transparente quando está na posição central. Assim, você pode se concentrar em um módulo por vez, o que melhora a experiência.

Performance da página

Passar de um protótipo funcional para uma versão de lançamento sem instabilidade significa ir de adivinhar para saber o que acontece no navegador. O Chrome DevTools é seu melhor amigo.

Passamos muito tempo otimizando o site. Forçar a aceleração de hardware é uma das ferramentas mais importantes, obviamente, para obter animações suaves. Também é possível procurar colunas coloridas e retângulos vermelhos no Chrome DevTools. Há muitos artigos bons sobre esses tópicos, e você deveria ler todos eles. A recompensa por remover frames pulados é instantânea, assim como a frustração quando eles retornam de novo. E eles farão isso. É um processo contínuo que precisa de iterações.

Gosto de usar o TweenMax do Greensock para propriedades de interpolação, transformações e CSS. Pense em contêineres e visualize a estrutura à medida que você adiciona camadas. Lembre-se de que as transformações atuais podem ser substituídas por novas. O translateZ(0) que forçou a aceleração de hardware na sua classe CSS será substituído por uma matriz 2D se você interpolar apenas valores 2D. Para manter a camada no modo de aceleração nesses casos, use a propriedade “force3D:true” na interpolação para criar uma matriz 3D em vez de uma matriz 2D. É fácil esquecer quando você combina interpolações de CSS e JavaScript para definir estilos.

Não force a aceleração de hardware onde ela não for necessária. A memória da GPU pode ser preenchida rapidamente e causar resultados indesejados quando você quer acelerar muitos contêineres, principalmente no iOS, em que a memória tem mais restrições. Fizemos grandes melhorias para carregar recursos menores e aumentá-los com CSS e desativar alguns dos efeitos no modo para dispositivos móveis.

Vazamentos de memória era outro campo em que precisávamos melhorar nossas habilidades. Ao navegar entre as diferentes experiências WebGL, muitos objetos, materiais, texturas e geometrias são criados. Se eles não estiverem prontos para a coleta de lixo quando você sair e remover a seção, provavelmente farão com que o dispositivo falhe depois de um tempo quando ficar sem memória.

Sair de uma seção com uma função de descarte com falha.
Saída de uma seção com uma função de descarte com falha.
Muito melhor!
Muito melhor!

Para encontrar o vazamento, foi um fluxo de trabalho bastante simples no DevTools, gravando a linha do tempo e capturando instantâneos da pilha. É mais fácil se houver objetos específicos, como geometria 3D ou uma biblioteca específica, que você possa filtrar. No exemplo acima, notou-se que a cena em 3D ainda existia e que uma matriz que armazenava a geometria não foi apagada. Se você estiver com dificuldade para encontrar o local do objeto, use um recurso chamado caminhos de retenção. Basta clicar no objeto que deseja inspecionar no instantâneo de heap e você obterá as informações em um painel abaixo. Usar uma boa estrutura com objetos menores ajuda a localizar as referências.

A cena foi referenciada no EffectComposer.
A cena foi referenciada no EffectComposer.

Em geral, é saudável pensar duas vezes antes de manipular o DOM. Ao fazer isso, pense na eficiência. Se puder, não manipule o DOM dentro de um loop de jogo. Armazene referências em variáveis para reutilização. Se você precisar pesquisar um elemento, use a rota mais curta armazenando referências a contêineres estratégicos e pesquisando no elemento ancestral mais próximo.

Atrase a leitura das dimensões de elementos recém-adicionados ou ao remover/adicionar classes se houver bugs de layout. Ou verifique se o Layout é acionado. Às vezes, o lote do navegador muda para estilos e não é atualizado após o próximo gatilho de layout. Isso pode ser um grande problema às vezes, mas está lá por uma razão, então tente aprender como ele funciona nos bastidores e você ganhará muito.

Tela cheia

Quando disponível, você tem a opção de colocar o site no modo de tela cheia no menu por meio da API Fullscreen. No entanto, em dispositivos móveis, os navegadores também decidiram colocá-lo em tela cheia. Anteriormente, o Safari no iOS tinha um hack para permitir que você controlasse isso, mas isso não está mais disponível. Assim, é necessário preparar seu design para funcionar sem ele ao criar uma página sem rolagem. Provavelmente haverá atualizações sobre isso nas próximas atualizações, já que muitos apps da Web foram corrompidos.

Recursos

Instruções animadas para os experimentos.
Instruções animadas para os experimentos

Em todo o site, temos muitos tipos diferentes de recursos, nós usamos imagens (PNG e JPEG), SVG (in-line e plano de fundo), spritesheets (PNG), fontes de ícones personalizadas e animações do Adobe Edge. Usamos PNGs para recursos e animações (Folhas de sprite) em que o elemento não pode ser baseado em vetor. Caso contrário, tentamos usar SVGs o máximo possível.

O formato vetorial significa que não há perda de qualidade, mesmo se dimensioná-la. Um arquivo para todos os dispositivos.

  • O arquivo é pequeno.
  • Podemos animar cada parte separadamente (perfeito para animações avançadas). Como exemplo, ocultamos o "subtítulo" do logotipo do Hobbit (a desolação de Smaug) quando ele é reduzido.
  • Ele pode ser incorporado como uma tag HTML SVG ou usado como uma imagem de plano de fundo sem carregamento extra (ele é carregado ao mesmo tempo que a página html).

As famílias tipográficas de ícones têm as mesmas vantagens do SVG em termos de escalonabilidade e são usadas em vez do SVG para elementos pequenos, como ícones, em que só precisamos mudar a cor (passar o cursor, ativar etc.). Os ícones também são muito fáceis de reutilizar, basta definir a propriedade "content" do CSS de um elemento.

Animações

Em alguns casos, animar elementos SVG com código pode levar muito tempo, especialmente quando a animação precisa ser bastante alterada durante o processo de design. Para melhorar o fluxo de trabalho entre designers e desenvolvedores, usamos o Adobe Edge para algumas animações (as instruções antes dos jogos). O fluxo de trabalho de animação é bem semelhante ao Flash e isso ajudou a equipe, mas há algumas desvantagens, especialmente com a integração de animações do Edge em nosso processo de carregamento de ativos, já que ele vem com seus próprios carregadores e lógica de implementação.

Ainda sinto que ainda temos um longo caminho pela frente até termos um fluxo de trabalho perfeito para lidar com recursos e animações feitas à mão na Web. Estamos ansiosos para ver como ferramentas como o Edge vão evoluir. Fique à vontade para adicionar sugestões sobre outras ferramentas de animação e fluxos de trabalho nos comentários.

Conclusão

Agora, quando todas as partes do projeto são lançadas e analisamos o resultado final, devo dizer que estamos bastante impressionados com o estado dos navegadores para dispositivos móveis modernos. Quando começamos este projeto, tínhamos expectativas muito menores sobre a qualidade, integração e desempenho que poderíamos realizar. Tem sido uma ótima experiência de aprendizado para nós, e todo o tempo gasto com iterações e testes (muito) melhorou nossa compreensão de como os navegadores modernos funcionam. É isso que precisamos fazer se quisermos encurtar o tempo de produção desses tipos de projetos, passando de adivinhação para conhecimento.