Estudo de caso: criação do doodle Stanisław Lem do Google

Marcin Wichary
Marcin Wichary

Olá, (estranho) mundo

A página inicial do Google é um ambiente fascinante para programar. Ele vem com muitas restrições desafiadoras: foco especial em velocidade e latência, atendendo a todos os tipos de navegadores e trabalhando em várias circunstâncias, e… sim, surpreender e encantar.

Estou falando sobre os Google Doodles, as ilustrações especiais que às vezes substituem nosso logotipo. E embora meu relacionamento com canetas e pincéis tenha o sabor distinto de uma ordem de restrição, muitas vezes eu contribuo com as interativas.

Todos os doodles interativos que programei (Pac-Man, Jules Verne, World's Fair) e muitos outros em que ajudei eram futuristas e anacrônicos: grandes oportunidades para aplicações ousadas de recursos avançados da Web e pragmatismo ousado de compatibilidade entre navegadores.

Aprendemos muito com cada doodle interativo, e o minijogo recente de Stanisław Lem não foi exceção, com 17.000 linhas de código JavaScript testando muitas coisas pela primeira vez na história do Doodle. Hoje, quero compartilhar esse código com você. Talvez você encontre algo interessante ou aponte meus erros. Vamos falar um pouco sobre isso.

Confira o código do Doodle do Stanisław Lem »

Vale lembrar que a página inicial do Google não é um lugar para demonstrações de tecnologia. Com nossos doodles, queremos celebrar pessoas e eventos específicos, e queremos fazer isso usando a melhor arte e as melhores tecnologias que podemos usar, mas nunca celebramos a tecnologia por causa da tecnologia. Isso significa analisar cuidadosamente qualquer parte do HTML5 amplamente compreendido que esteja disponível e se ele ajuda a melhorar o desenho sem distrair ou ofuscar.

Vamos conferir algumas das tecnologias modernas da Web que encontraram seu lugar e outras que não encontraram no doodle de Stanisław Lem.

Gráficos usando DOM e tela

O Canvas é poderoso e foi criado para exatamente o tipo de coisas que queríamos fazer neste doodle. No entanto, alguns dos navegadores mais antigos que nos interessavam não ofereciam suporte a ele. Mesmo que eu esteja literalmente compartilhando um escritório com a pessoa que criou uma excelente excanvas, decidi escolher outra maneira.

Eu montei um motor gráfico que abstrai primitivas gráficas chamadas "rects" e as renderiza usando canvas ou DOM se a canvas estiver indisponível.

Essa abordagem apresenta alguns desafios interessantes. Por exemplo, mover ou mudar um objeto no DOM tem consequências imediatas, enquanto para a tela, há um momento específico em que tudo é desenhado ao mesmo tempo. Eu decidi ter apenas uma tela, limpá-la e desenhar do zero com cada frame. Muitas partes móveis, literalmente, por um lado, e por outro, não há complexidade suficiente para justificar a divisão em várias telas sobrepostas e a atualização seletiva delas.)

Infelizmente, mudar para a tela não é tão simples quanto espelhar planos de fundo do CSS com drawImage(): você perde várias coisas que são sem custo financeiro ao juntar elementos pelo DOM, principalmente a criação de camadas com z-indexes e eventos do mouse.

Eu já abstraí o índice z com um conceito chamado "planos". O desenho definiu vários planos, do céu ao fundo, ao cursor do mouse na frente de tudo, e cada ator no desenho precisava decidir a qual plano ele pertencia. Pequenas correções de mais/menos em um plano foram possíveis usando planeCorrection.

Na renderização pelo DOM, os planos são simplesmente traduzidos para o índice z. No entanto, se renderizarmos pela tela, precisamos classificar os retângulos com base nos planos antes de desenhá-los. Como isso é caro, a ordem é recalculada apenas quando um ator é adicionado ou quando ele se move para outro plano.

Para eventos de mouse, eu também abstraí isso... mais ou menos. Para DOM e tela, usei outros elementos DOM flutuantes completamente transparentes com z-index alto, cuja função é apenas reagir a movimentos do mouse, cliques e toques.

Uma das coisas que queríamos tentar com esse doodle era quebrar a quarta parede. O mecanismo acima nos permitiu combinar atores baseados em tela com atores baseados em DOM. Por exemplo, as explosões no final estão na tela para objetos no universo e no DOM para o restante da página inicial do Google. O pássaro, que normalmente voa por aí e é cortado pela nossa máscara irregular como qualquer outro ator, decide ficar longe de problemas durante o nível de filmagem e se senta no botão "Estou com sorte". A maneira como isso é feito é para que o pássaro saia da tela e se torne um elemento DOM (e vice-versa mais tarde), o que eu esperava que fosse completamente transparente para nossos visitantes.

A taxa de frames

Conhecer a taxa de frames atual e reagir quando ela está muito lenta (e muito rápida!) foi uma parte importante do nosso mecanismo. Como os navegadores não informam a taxa de frames, precisamos calcular isso por conta própria.

Comecei usando requestAnimationFrame, retornando ao setTimeout antigo se o primeiro não estivesse disponível. O requestAnimationFrame salva a CPU de forma inteligente em algumas situações, embora estejamos fazendo isso, como será explicado abaixo, mas também nos permite obter uma taxa de frames mais alta do que setTimeout.

Calcular a taxa de frames atual é simples, mas está sujeito a mudanças drásticas. Por exemplo, ela pode cair rapidamente quando outro aplicativo ocupa o computador por um tempo. Portanto, calculamos uma taxa de frame "flutuante" (média) apenas a cada 100 ticks físicos e tomamos decisões com base nisso.

Que tipo de decisões?

  • Se a taxa de frames for maior que 60 fps, ela será limitada. No momento, o requestAnimationFrame em algumas versões do Firefox não tem limite máximo na taxa de frames, e não há sentido em desperdiçar a CPU. Na verdade, o limite é de 65 fps, devido aos erros de arredondamento que fazem a taxa de frames ser um pouco maior que 60 fps em outros navegadores. Não queremos começar a limitar isso por engano.

  • Se a taxa de frames for menor que 10qps, simplesmente diminuímos a velocidade do mecanismo em vez de descartar frames. É uma proposta de perda-perda, mas senti que pular frames excessivamente seria mais confuso do que simplesmente ter um jogo mais lento (mas ainda coerente). Há outro efeito colateral positivo disso: se o sistema ficar lento temporariamente, o usuário não vai notar um salto estranho enquanto o mecanismo está tentando acompanhar. Fiz isso de forma um pouco diferente para Pac-Man, mas a taxa de frames mínima é uma abordagem melhor.

  • Por fim, podemos pensar em simplificar os gráficos quando a taxa de frames fica perigosamente baixa. Não estamos fazendo isso para o doodle do Lem, com exceção do cursor do mouse (mais informações abaixo), mas, hipoteticamente, poderíamos perder algumas das animações externas para que o doodle fique fluido mesmo em computadores mais lentos.

Também temos o conceito de marcação física e lógica. O primeiro vem de requestAnimationFrame/setTimeout. A proporção no jogo normal é 1:1, mas para avançar rapidamente, basta adicionar mais tiques lógicos por tique físico (até 1:5). Isso nos permite fazer todos os cálculos necessários para cada marcação lógica, mas apenas designar a última como a que atualiza as coisas na tela.

Comparativo de mercado

Uma suposição pode ser feita (e de fato, foi feita no início) de que a tela será mais rápida que o DOM sempre que estiver disponível. Isso nem sempre é verdade. Durante os testes, descobrimos que o Opera 10.0–10.1 em um Mac e o Firefox no Linux são mais rápidos ao mover elementos DOM.

No mundo perfeito, o Doodle compararia silenciosamente diferentes técnicas gráficas: elementos DOM movidos usando style.left e style.top, desenhos na tela e talvez até elementos DOM movidos usando transformações CSS3.

– e depois mude para a que tiver a maior taxa de frames. Comecei a escrever o código para isso, mas descobri que minha maneira de fazer comparações de mercado era bastante imprecisa e exigia muito tempo. Tempo que não temos na página inicial. Valorizamos muito a velocidade e queremos que o doodle apareça instantaneamente e que a jogabilidade comece assim que você clicar ou tocar.

No final, o desenvolvimento da Web às vezes se resume a fazer o que precisa ser feito. Olhei por cima do ombro para ter certeza de que ninguém estava olhando e depois codifiquei o Opera 10 e o Firefox fora da tela. Na próxima vida, vou voltar como uma tag <marquee>.

Como economizar CPU

Você conhece aquele amigo que vem à sua casa, assiste o final da temporada de Breaking Bad, estraga tudo para você e depois apaga da sua DVR? Você não quer ser essa pessoa, não é?

Então, sim, a pior analogia de todos os tempos. Mas também não queremos que nosso doodle seja esse cara. O fato de termos permissão para acessar a guia do navegador de alguém é um privilégio, e acumular ciclos de CPU ou distrair o usuário nos tornaria um convidado desagradável. Portanto, se ninguém estiver brincando com o Doodle (sem toques, cliques, movimentos do mouse ou pressionamentos de tecla), queremos que ele vá para a suspensão.

Quando?

  • após 18 segundos na página inicial (os jogos de fliperama chamam isso de modo de atração).
  • após 180 segundos, se a guia tiver foco
  • após 30 segundos, se a guia não tiver foco (por exemplo, o usuário mudou para outra janela, mas talvez ainda esteja assistindo o doodle em uma guia inativa)
  • Imediatamente, se a guia ficar invisível (por exemplo, o usuário mudou para outra guia na mesma janela. Não há sentido em desperdiçar ciclos se não for possível ser visto)

Como saber se a guia está ativa? Nos conectamos a window.focus e window.blur. Como sabemos que a guia está visível? Estamos usando a nova API Page Visibility e reagendo ao evento apropriado.

Os tempos limite acima são mais flexíveis do que o normal. Eu as adaptei para este doodle específico, que tem muitas animações ambientais (principalmente o céu e o pássaro). O ideal seria que os timeouts fossem limitados à interação no jogo, por exemplo, logo após o pouso, o pássaro poderia informar ao desenho que ele pode dormir agora, mas eu não implementei isso no final.

Como o céu está sempre em movimento, quando a pessoa adormece e acorda, o desenho não para ou começa, ele desacelera antes de pausar e vice versa para retomar, aumentando ou diminuindo o número de tiques lógicos por um tique físico, conforme necessário.

Transições, transformações, eventos

Um dos poderes do HTML sempre foi o fato de que você pode melhorá-lo por conta própria: se algo não for bom o suficiente no portfólio regular de HTML e CSS, você pode usar o JavaScript para fazer isso. Infelizmente, muitas vezes isso significa ter que começar do zero. As transições CSS3 são ótimas, mas não é possível adicionar um novo tipo de transição nem usar transições para fazer qualquer coisa além de estilizar elementos. Outro exemplo: as transformações CSS3 são ótimas para DOM, mas, quando você muda para a tela, você fica por conta própria.

Esses problemas e outros são os motivos pelos quais o Lemdoodle tem um próprio mecanismo de transição e transformação. Sim, eu sei, os anos 2000 ligaram, etc. Os recursos que criei não são tão poderosos quanto o CSS3, mas o que o mecanismo faz é consistente e nos dá muito mais controle.

Comecei com um sistema de ação (evento) simples: uma linha do tempo que dispara eventos no futuro sem usar setTimeout, já que em qualquer ponto o tempo de rabiscar pode se separar do tempo físico à medida que ele fica mais rápido (avanço rápido), mais lento (taxa de frames baixa ou desligamento para economizar CPU) ou para esperar o carregamento das imagens terminar.

As transições são apenas outro tipo de ação. Além dos movimentos básicos e da rotação, também oferecemos suporte a movimentos relativos (por exemplo, mover algo 10 pixels para a direita), coisas personalizadas, como tremores, e animações de imagem de frame-chave.

Mencionei rotações, e elas também são feitas manualmente: temos sprites para vários ângulos dos objetos que precisam ser girados. O motivo principal é que as rotações de CSS3 e canvas estavam introduzindo artefatos visuais que consideramos inaceitáveis. Além disso, esses artefatos variavam de acordo com a plataforma.

Como alguns objetos que giram estão conectados a outros objetos giratórios, por exemplo, a mão de um robô conectada ao braço inferior, que está conectado a um braço superior giratório, também precisei criar uma transformação de origem de um homem pobre na forma de pivôs.

Tudo isso é uma quantidade sólida de trabalho que, em última análise, abrange o terreno já cuidado pelo HTML5. No entanto, às vezes, o suporte nativo não é bom o suficiente, e é hora de reinventar a roda.

Como lidar com imagens e sprites

Um mecanismo não serve apenas para executar o doodle, mas também para trabalhar nele. Compartilhei alguns parâmetros de depuração acima. Você pode encontrar o restante em engine.readDebugParams.

Spriting é uma técnica conhecida que também usamos para rabiscos. Isso nos permite salvar bytes e diminuir os tempos de carregamento, além de facilitar o pré-carregamento. No entanto, isso também dificulta o desenvolvimento. Cada mudança nas imagens exigiria um novo espírito (em grande parte automatizado, mas ainda complicado). Portanto, o mecanismo oferece suporte à execução em imagens brutas para desenvolvimento, bem como sprites para produção via engine.useSprites. Ambos são incluídos no código-fonte.

Doodle do Pac-Man
Sprites usados pelo doodle do Pac-Man.

Também oferecemos suporte ao pré-carregamento de imagens à medida que avançamos e à interrupção do Doodle se as imagens não forem carregadas a tempo, com uma barra de progresso falsa. Faux porque, infelizmente, nem mesmo o HTML5 pode informar quanto de um arquivo de imagem já foi carregado.

Uma captura de tela do gráfico de carregamento com a barra de progresso fictícia.
Uma captura de tela do gráfico de carregamento com a barra de progresso fictícia.

Para algumas cenas, usamos mais de um sprite não para acelerar o carregamento usando conexões paralelas, mas simplesmente por causa da limitação de 3/5 milhões de pixels para imagens no iOS.

Onde o HTML5 se encaixa nisso tudo? Não há muito disso acima, mas a ferramenta que escrevi para o spriting/recorte foi toda nova tecnologia da Web: canvas, blobs, a[download]. Uma das coisas mais legais do HTML é que ele lentamente incorpora coisas que antes precisavam ser feitas fora do navegador. A única parte que precisávamos fazer era otimizar arquivos PNG.

Como salvar o estado entre jogos

Os mundos de Lem sempre pareciam grandes, vivos e realistas. As histórias dele geralmente começavam sem muitas explicações, com a primeira página começando em medias res, com o leitor tendo que se virar por conta própria.

O Cyberiad não foi uma exceção, e queríamos replicar essa sensação no doodle. Começamos tentando não explicar demais a história. Outra parte importante é a aleatoriedade, que achamos que se encaixava na natureza mecânica do universo do livro. Temos várias funções auxiliares que lidam com a aleatoriedade que usamos em muitos lugares.

Também queríamos aumentar a rejogabilidade de outras maneiras. Para isso, precisamos saber quantas vezes o doodle foi concluído antes. A solução tecnológica correta para isso é um cookie, mas isso não funciona para a página inicial do Google. Cada cookie aumenta o payload de cada página, e, novamente, nos preocupamos muito com velocidade e latência.

Felizmente, o HTML5 oferece o armazenamento Web, fácil de usar, que permite salvar e recuperar a contagem geral de exibições e a última cena reproduzida pelo usuário, com muito mais facilidade do que os cookies.

O que fazemos com essas informações?

  • Mostramos um botão de avanço rápido, permitindo pular as cenas que o usuário já assistiu
  • Mostramos N itens diferentes durante a final
  • aumentamos um pouco a dificuldade do nível de tiro
  • Mostramos um pequeno dragão de easter egg de uma história diferente na terceira vez que você joga e nas próximas vezes

Há vários parâmetros de depuração que controlam isso:

  • ?doodle-debug&doodle-first-run: finja que é a primeira execução
  • ?doodle-debug&doodle-second-run: finja que é uma segunda execução
  • ?doodle-debug&doodle-old-run: finja que é uma execução antiga

Dispositivos por toque

Queríamos que o Doodle parecesse em casa em dispositivos com tela touch. Os mais modernos são poderosos o suficiente para que o Doodle funcione muito bem, e jogar o jogo tocando na tela é muito mais divertido do que clicar.

Algumas mudanças iniciais na experiência do usuário precisavam ser feitas. Originalmente, o cursor do mouse era o único lugar que comunicava que uma cena/parte não interativa estava acontecendo. Mais tarde, adicionamos um pequeno indicador no canto inferior direito para não depender apenas do cursor do mouse, já que ele não existe em dispositivos com tela touch.

Normal Ocupado Clicável Clicado
Trabalho em andamento
Ponteiro normal de trabalho em andamento
Ponteiro de trabalho em andamento
Ponteiro clicável de trabalho em andamento
Ponteiro clicado em &quot;Work in progress&quot;
Final
Ponteiro normal finalv
Indicador de atividade final
Ponteiro clicável final
Ponteiro final clicado
Ponteiros do mouse durante o desenvolvimento e equivalentes finais.

A maioria das coisas funcionou imediatamente. No entanto, testes de usabilidade improvisados e rápidos da nossa experiência de toque mostraram dois problemas: alguns dos alvos eram muito difíceis de pressionar, e toques rápidos eram ignorados, já que substituímos os eventos de clique do mouse.

Ter elementos DOM transparentes clicáveis separados ajudou muito aqui, porque pude redimensioná-los independentemente dos recursos visuais. Acrescentei um padding extra de 15 pixels para dispositivos com tela touch e o usei sempre que elementos clicáveis eram criados. Adicionei um padding de 5 pixels para ambientes de mouse também, só para deixar o Sr. Fitts feliz.

Quanto ao outro problema, apenas anexei e testei os manipuladores de início e fim de toque adequados, em vez de depender do clique do mouse.

Também estamos usando propriedades de estilo mais modernas para remover alguns recursos de toque que os navegadores WebKit adicionam por padrão (destaque de toque, chamada de toque).

E como detectamos se um determinado dispositivo que executa o doodle oferece suporte ao toque? Preguiçosamente. Em vez de descobrir isso a priori, usamos nossos QIs combinados para deduzir que o dispositivo oferece suporte ao toque... depois de receber o primeiro evento de início de toque.

Personalizar o ponteiro do mouse

Mas nem tudo é baseado em toque. Um dos nossos princípios orientadores foi colocar o máximo de coisas possível no universo do doodle. A pequena interface da barra lateral (avanço rápido, ponto de interrogação), a dica de ferramenta e até o ponteiro do mouse.

Como personalizar um ponteiro do mouse? Alguns navegadores permitem alterar o cursor do mouse vinculando-o a um arquivo de imagem personalizado. No entanto, isso não tem suporte adequado e também é um pouco restritivo.

Se não for esse, qual é? Por que não fazer do ponteiro do mouse apenas mais um ator no doodle? Isso funciona, mas tem algumas desvantagens, principalmente:

  • Você precisa remover o ponteiro do mouse nativo.
  • você precisa ser muito bom em manter o ponteiro do mouse sincronizado com o "real"

O primeiro é complicado. O CSS3 permite cursor: none, mas ele também não é compatível com alguns navegadores. Precisamos recorrer a algumas ginásticas: usar um arquivo .cur vazio como substituto, especificar o comportamento concreto para alguns navegadores e até mesmo codificar outros fora da experiência.

O outro é relativamente trivial, mas, com o ponteiro do mouse sendo apenas outra parte do universo do desenho, ele também herda todos os problemas. O maior deles? Se a taxa de frames do doodle for baixa, a taxa de frames do ponteiro do mouse também será baixa, o que tem consequências terríveis, já que o ponteiro do mouse, sendo uma extensão natural da mão, precisa ser responsivo de qualquer jeito. (As pessoas que usaram o Commodore Amiga no passado estão acenando vigorosamente.)

Uma solução um pouco complexa para esse problema é separar o ponteiro do mouse do loop de atualização regular. Fizemos exatamente isso, em um universo alternativo em que eu não preciso dormir. Uma solução mais simples para isso? Basta reverter para o ponteiro do mouse nativo se a taxa de frames por segundo cair abaixo de 20 fps. É aí que a taxa de frames rolante é útil. Se reagirmos à taxa de frames atual e se ela oscilar em torno de 20 fps, o usuário vai notar que o ponteiro do mouse personalizado vai aparecer e desaparecer o tempo todo.) Isso nos leva a:

Intervalo de frame rate Comportamento
>10fps Desacelerar o jogo para que mais frames não sejam descartados.
10 a 20 fps Use o ponteiro do mouse nativo em vez do personalizado.
20 a 60 fps Operação normal.
>60fps Limite a taxa de frames para que ela não exceda esse valor.
Resumo do comportamento dependente da taxa de frames.

Ah, e o ponteiro do mouse é escuro em um Mac, mas branco em um PC. Por quê? Porque as guerras de plataformas precisam de combustível, mesmo em universos fictícios.

Conclusão

Não é um mecanismo perfeito, mas não tenta ser. Ele foi desenvolvido junto com o doodle do Lem e é muito específico para ele. Tudo bem. "A otimização prematura é a raiz de todo mal", como disse Don Knuth, e não acho que escrever um mecanismo isoladamente primeiro e só aplicá-lo mais tarde faz sentido. A prática informa a teoria tanto quanto a teoria informa a prática. No meu caso, o código foi descartado, várias partes foram reescritas várias vezes, e muitas peças comuns foram notadas após a postagem, em vez de ante factum. Mas, no final, o que temos aqui nos permitiu fazer o que queríamos: celebrar a carreira de Stanisław Lem e os desenhos de Daniel Mróz da melhor maneira possível.

Esperamos que as informações acima tenham esclarecido algumas das escolhas de design e compensações que precisamos fazer e como usamos o HTML5 em um cenário específico e real. Agora, teste o código-fonte e nos diga o que você achou.

Eu mesmo fiz isso. O exemplo abaixo estava ativo nos últimos dias, contando regressivamente para as primeiras horas do dia 23 de novembro de 2011 na Rússia, que foi o primeiro fuso horário a ver o doodle do Lem. Uma coisa, talvez, mas, assim como os rabiscos, coisas que parecem insignificantes às vezes têm um significado mais profundo. Esse contador foi uma boa "prova de estresse" para o mecanismo.

Captura de tela do relógio de contagem regressiva do Doodle do Lem.
Captura de tela do relógio de contagem regressiva do Doodle do Lem.

E essa é uma maneira de olhar para a vida de um Doodle do Google: meses de trabalho, semanas de testes, 48 horas de cozimento, tudo para algo que as pessoas jogam por cinco minutos. Cada uma das milhares de linhas de JavaScript espera que esses cinco minutos sejam bem aproveitados. Aproveite.