O front-end da Terra-média

Um tutorial de desenvolvimento multidispositivo

No nosso primeiro artigo sobre o desenvolvimento do Experimento do Chrome Viagem pela Terra-média, focamos no desenvolvimento do WebGL para dispositivos móveis. Neste artigo, discutimos os desafios, problemas e soluções que encontramos ao criar o 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 dos recursos do dispositivo.

Todo o projeto é baseado em um estilo muito "cinematográfico", em que queríamos manter a experiência em um frame fixo orientado para paisagem para manter a magia do filme. Como uma grande parte do projeto consiste em minijogos interativos, não faz sentido permitir que eles transbordem o frame.

Podemos usar a página de destino como exemplo de como adaptar o design para tamanhos diferentes.

As águias acabaram de nos levar à página de destino.
As águias acabaram de nos deixar na página de destino.

O site tem três modos diferentes: computador, tablet e dispositivo móvel. Não apenas para processar o layout, mas porque precisamos processar recursos carregados no momento da execução e adicionar várias otimizações de desempenho. Com dispositivos que têm uma resolução maior do que computadores e laptops, mas têm desempenho pior do que os smartphones, não é fácil definir o conjunto de regras final.

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

Como os designs neste caso não são baseados em grades ou regras e são bastante exclusivos entre as diferentes seções, depende muito do elemento e do cenário específico quais pontos de interrupção ou estilos usar. Já aconteceu mais de uma vez que configuramos o layout perfeito com bons sass-mixins e consultas de mídia e depois precisamos 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 usar essas informações nos 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 ao criar uma experiência imersiva na Web. No computador, temos um tamanho mínimo antes de mostrar as barras de rolagem porque queremos que você acesse o site em uma viewport maior, se possível. Em dispositivos móveis, decidimos permitir o modo paisagem e retrato até as experiências interativas, em que pedimos para você girar o dispositivo para a orientação paisagem. O argumento contra isso foi que o site não é tão imersivo no modo retrato quanto no modo paisagem. No entanto, o site foi dimensionado muito bem, então 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 precisam abranger todos eles. Um exemplo é oferecer suporte a mouse e toque ao mesmo tempo. A compensação da retina é para a qualidade, mas a maioria de todas as performances é outra. Às vezes, a qualidade menor é melhor. Por exemplo, a tela tem metade da resolução nas experiências do WebGL em telas Retina, que renderizariam quatro vezes o número de pixels.

Usamos com frequência a ferramenta de emulador no DevTools durante o desenvolvimento, especialmente 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 é que o site está se adaptando ao modo tela cheia. As páginas com rolagem vertical ocultam a interface do navegador na maioria dos casos (o Safari no iOS7 tem problemas com isso no momento), mas precisamos encaixar tudo independente disso. Também usamos uma predefinição no emulador e mudamos a configuração do 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, chegamos ao mapa da Terra-média. Você notou a mudança no URL? O site é um aplicativo de página única que usa a API History para processar o roteamento.

Cada seção do site é um objeto que herda um modelo de funcionalidade, como elementos DOM, transições, carregamento de recursos, descarte etc. 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 clicar no botão "Voltar" do navegador ou navegar pelo menu a qualquer momento, tudo o que for criado precisa ser descartado em algum momento. Os tempos limite e as animações precisam ser interrompidos e descartados, caso contrário, vão causar comportamento indesejado, erros e vazamentos de memória. Isso nem sempre é uma tarefa fácil, especialmente quando os prazos estão se aproximando e você precisa colocar tudo lá o mais rápido possível.

Mostrando os locais

Para mostrar as belas paisagens e os personagens da Terra-média, criamos um sistema modular de componentes de imagem e texto que podem ser arrastados ou deslizados horizontalmente. Não ativamos a barra de rolagem aqui porque queremos ter velocidades diferentes em diferentes intervalos, como em sequências de imagens em que você interrompe o movimento lateralmente até que o clipe seja reproduzido.

Salão de Thranduil
Cronologia do Salão de Thranduil

A linha do tempo

Quando o desenvolvimento começou, não sabíamos o conteúdo dos módulos de cada local. O que sabíamos era que queríamos uma maneira padronizada 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 de local diferentes sem precisar reconstruir tudo seis vezes. Para gerenciar isso, criamos um controlador de linha do tempo que processa a movimentação dos módulos com base nas configurações e nos comportamentos deles.

Módulos e componentes de comportamento

Os diferentes módulos que adicionamos suporte são sequência de imagens, imagem estática, cena de paralaxe, cena de mudança de foco e texto.

O módulo de cena de paralaxe tem um plano de fundo opaco com um número personalizado de camadas que detectam 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 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 é muito caro. Por isso, vamos esperar pelos sombreadores do CSS.

O conteúdo no módulo de texto é arrastável com o plug-in Draggable do TweenMax. Você também pode usar a roda de rolagem ou deslizar com dois dedos para rolar verticalmente. Observe o throw-props-plugin, que adiciona a física de estilo de movimento rápido quando você desliza e solta.

Os módulos também podem ter comportamentos diferentes que são adicionados como um conjunto de componentes. Todos têm os próprios seletores e configurações de destino. Traduzir para mover um elemento, dimensionar para aumentar, pontos quentes para sobrepor informações, depurar métricas para testar visualmente, sobrepor o título de início, uma camada de brilho e muito mais. Elas serão anexadas ao DOM ou controlarão o elemento de destino no módulo.

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

Sequências de imagens

O módulo mais desafiador em termos de desempenho e tamanho de download é a sequência de imagens. Há muito o que ler sobre esse assunto. Em dispositivos móveis e tablets, substituímos isso por uma imagem estática. São 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 uma imagem de plano de fundo e uma spritesheet primeiro, mas isso levou a problemas de memória e atraso quando a GPU precisava trocar entre spritesheets. Depois, tentamos trocar os elementos img, mas também era muito lento. Desenhar um frame de uma spritesheet para uma tela foi o que teve melhor desempenho, então começamos a otimizar isso. Para economizar tempo de computação em cada frame, os dados da imagem a serem gravados na tela são pré-processados usando uma tela temporária e salvos com putImageData() em uma matriz, decodificados e prontos para uso. A spritesheet original pode ser coletada e armazenamos apenas a quantidade mínima de dados necessários na memória. Talvez seja menor o armazenamento de imagens não decodificadas, mas a performance é melhor ao reproduzir a sequência dessa maneira. Os frames são bem pequenos, apenas 640x400, mas eles só vão ficar visíveis durante a reprodução. Quando você para, uma imagem de 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 sprites são geradas com o Imagemagick. Confira um exemplo simples no GitHub que mostra como criar uma spritesheet 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 dela, exibida fora da tela, rastreia o "cursor" e a largura da linha do tempo. Isso pode ser feito apenas com código, mas foi bom ter uma representação visual durante o desenvolvimento e a depuração. Quando executado de verdade, ele é atualizado apenas no redimensionamento para definir dimensões. Alguns módulos preenchem a viewport e outros têm a 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 fosse cortado demais. 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 o movimento de paralaxe, muitas vezes é difícil calcular a posição inicial e final dos objetos para sincronizar com a posição esperada quando ela está em exibição. É bom saber exatamente quando um módulo entra na visualização, reproduz a linha do tempo interna e quando ele sai da visualização novamente.

Cada módulo tem uma camada preta sutil na parte de cima que ajusta a opacidade para que fique totalmente transparente quando estiver na posição central. Isso ajuda você a se concentrar em um módulo por vez, o que melhora a experiência.

Desempenho da página

A mudança de um protótipo funcional para uma versão de lançamento sem instabilidade significa passar de suposições para saber o que acontece no navegador. É aí que o Chrome DevTools é seu melhor amigo.

Passamos muito tempo otimizando o site. Forçar a aceleração de hardware é uma das ferramentas mais importantes para conseguir animações suaves. Mas também procure colunas coloridas e retângulos vermelhos no Chrome DevTools. Há muitos artigos bons sobre os temas, e você precisa ler todos. A recompensa por remover os frames de salto é instantânea, mas a frustração quando eles voltam também. E vai acontecer. É 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 sua estrutura à medida que adiciona novas camadas. 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ê usar apenas valores 2D. Para manter a camada no modo de aceleração nesses casos, use a propriedade "force3D:true" no tween para criar uma matriz 3D em vez de uma matriz 2D. É fácil esquecer quando você combina tweens de CSS e JavaScript para definir estilos.

Não force a aceleração de hardware quando ela não for necessária. A memória da GPU pode se encher rapidamente e causar resultados indesejados quando você quer acelerar muitos contêineres por hardware, especialmente no iOS, onde a memória tem mais restrições. Para carregar recursos menores e dimensioná-los com CSS e desativar alguns efeitos no modo móvel, foram feitas melhorias significativas.

Vazamentos de memória foi outro campo em que precisávamos melhorar nossas habilidades. Ao navegar entre as diferentes experiências do 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 causarão falhas no dispositivo depois de algum tempo, quando ele ficar sem memória.

Saindo 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.
Bem melhor!
Muito melhor!

Para encontrar o vazamento, o fluxo de trabalho no DevTools foi bem direto, gravando a linha do tempo e capturando resumos de pilha. É mais fácil se houver objetos específicos, como geometria 3D ou uma biblioteca específica, que você possa filtrar. No exemplo acima, a cena 3D ainda estava presente, e uma matriz que armazenava a geometria não foi limpa. Se você tiver dificuldade para localizar o objeto, há um recurso útil que permite visualizar isso, chamado de caminhos de retenção. Basta clicar no objeto que você quer inspecionar no snapshot do heap para receber as informações em um painel abaixo. Usar uma boa estrutura com objetos menores ajuda a localizar suas referências.

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

Em geral, é recomendável pensar duas vezes antes de manipular o DOM. Ao fazer isso, pense na eficiência. Não manipule o DOM dentro de um loop de jogo, se possível. 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.

Atraso na leitura de dimensões de elementos recém-adicionados ou ao remover/adicionar classes se você encontrar bugs de layout. Ou confira se o layout está acionado. Às vezes, o lote do navegador muda para estilos e não é atualizado após o próximo acionador de layout. Às vezes, isso pode ser um grande problema, mas ele está lá por um motivo. Portanto, tente aprender como ele funciona nos bastidores e você vai ganhar muito.

Tela cheia

Quando disponível, você tem a opção de colocar o site no modo de tela cheia no menu usando a API Fullscreen. Mas nos dispositivos, há também a decisão do navegador de colocar em tela cheia. O Safari no iOS tinha uma solução alternativa para permitir esse controle, mas ela não está mais disponível. Portanto, você precisa preparar seu design para funcionar sem ela ao criar uma página sem rolagem. Provavelmente vamos receber atualizações sobre isso em atualizações futuras, já que o problema afetou muitos apps da Web.

Recursos

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

No site, temos muitos tipos diferentes de recursos. Usamos imagens (PNG e JPEG), SVG (inline e background), spritesheets (PNG), fontes de ícones personalizadas e animações do Adobe Edge. Usamos PNGs para recursos e animações (spritesheets) em que o elemento não pode ser baseado em vetores. Caso contrário, tentamos usar SVGs sempre que possível.

O formato vetorial não tem perda de qualidade, mesmo que seja dimensionado. 1 arquivo para todos os dispositivos.

  • Tamanho de arquivo pequeno.
  • Podemos animar cada parte separadamente, o que é perfeito para animações avançadas. Como exemplo, ocultamos o "legenda" 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 (é carregado ao mesmo tempo que a página HTML).

As famílias tipográficas de ícones têm as mesmas vantagens do SVG em relação à escalabilidade e são usadas em vez do SVG para elementos pequenos, como ícones em que só precisamos mudar a cor (passar o cursor, ativo 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 ser muito demorado, principalmente quando a animação precisa ser alterada várias vezes 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 é muito parecido com o do Flash, o que ajudou a equipe, mas há algumas desvantagens, especialmente na integração das animações do Edge no nosso processo de carregamento de recursos, já que ele vem com os próprios carregadores e a lógica de implementação.

Ainda temos um longo caminho a percorrer para ter um fluxo de trabalho perfeito para lidar com recursos e animações manuais na Web. Estamos ansiosos para ver como ferramentas como o Edge vão evoluir. Sinta-se à vontade para adicionar sugestões sobre outras ferramentas de animação e fluxos de trabalho nos comentários.

Conclusão

Agora que todas as partes do projeto foram lançadas e podemos analisar o resultado final, devo dizer que estamos bastante impressionados com o estado dos navegadores móveis modernos. Quando começamos este projeto, tínhamos expectativas muito menores sobre como ele seria simples, integrado e eficiente. Foi uma ótima experiência de aprendizado para nós, e todo o tempo gasto iterando e testando (muito) melhorou nosso entendimento de como os navegadores modernos funcionam. E é isso que vai ser necessário se quisermos encurtar o tempo de produção desses tipos de projetos, passando de suposições para conhecimento.