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

Marcin Wichary
Marcin Wichary

Olá, mundo (estranho)

A página inicial do Google é um ambiente fascinante para programar. Ela tem muitas restrições desafiadoras: foco particular na velocidade e latência, ter que atender a todos os tipos de navegadores e trabalho em várias circunstâncias e... sim, surpresa e prazer.

Estou falando dos doodles do Google, as ilustrações especiais que às vezes substituem nosso logotipo. Embora minha relação com canetas e pincéis tenha há muito tempo esse sabor característico de uma ordem de restrição, muitas vezes contribuo para os mais interativos.

Todos os doodles interativos que codifiquei (Pac-Man, Jules Verne, World’s Fair), e muitos com os quais ajudei, estavam em partes iguais futuristas e anacrônicas: grandes oportunidades para aplicações inusitadas de recursos da Web de ponta... e pragmatismo audacioso da compatibilidade entre navegadores.

Aprendemos muito com cada doodle interativo, e o recente minijogo de Stanisław Lem não foi exceção, com suas 17.000 linhas de código JavaScript fazendo muitas coisas pela primeira vez na história dos doodles. Hoje, quero compartilhar esse código com você (talvez você encontre algo interessante lá ou indique meus erros) e fale um pouco sobre isso.

Veja o código do doodle do Stanisław Lem »

Vale a pena ter em mente 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 fazer isso usando a melhor arte e as melhores tecnologias que podemos invocar, mas nunca celebrar a tecnologia em nome da tecnologia. Isso significa examinar cuidadosamente qualquer parte do HTML5 amplamente compreendido que esteja disponível e se ela nos ajuda a melhorar o doodle sem se distrair ou ofuscar.

Então, vamos ver algumas das tecnologias modernas da Web que se estabeleceram, e outras que não encontraram, no doodle Stanisław Lem.

Gráficos via DOM e canvas

O Canvas é poderoso e foi criado exatamente para o tipo de coisa que queríamos fazer nesse doodle. No entanto, alguns dos navegadores mais antigos que nos importamos não tinham suporte a ele. E mesmo que eu esteja literalmente compartilhando um escritório com a pessoa que criou um excanvas excelente, decidi escolher uma maneira diferente.

Criei um mecanismo gráfico que abstrai os primitivos gráficos chamados de "rects" e os renderiza usando canvas, DOM, se a tela não estiver disponível.

Essa abordagem tem alguns desafios interessantes. Por exemplo, mover ou mudar um objeto no DOM tem consequências imediatas, enquanto para canvas há um momento específico em que tudo é desenhado ao mesmo tempo. Decidi ter apenas uma tela, limpar e desenhar do zero a cada frame. Por um lado, há muitas peças móveis e, por outro, não há complexidade suficiente para justificar a divisão em várias telas sobrepostas e atualizá-las seletivamente.

Infelizmente, mudar para o canvas não é tão simples quanto simplesmente espelhar os planos de fundo CSS com drawImage(): você perde várias coisas que vêm sem custo financeiro ao montar tudo usando o DOM, principalmente a criação de camadas com Z-indexes e eventos de mouse.

Eu já abstraí o Z-index com um conceito chamado "planos". O doodle definiu vários planos, desde o céu muito atrás até o ponteiro do mouse na frente de tudo, e cada ator no doodle tinha que decidir a qual pertencia. Pequenas correções de mais/menos dentro de um plano eram possíveis usando planeCorrection.

Renderizando pelo DOM, os planos são simplesmente convertidos em Z-index. No entanto, se renderizarmos via canvas, vamos precisar classificar os retângulos com base nos planos deles antes de desenhá-los. Como isso sempre é caro, a ordem é recalculada apenas quando um ator é adicionado ou quando ele se move para outro plano.

Para eventos do mouse, eu absorvi isso também... mais ou menos. Tanto para o DOM quanto para a tela, usei outros elementos DOM flutuantes completamente transparentes com Z-index alto, cuja função é apenas reagir a ações de passar o mouse/fora da tela, cliques e toques.

Uma das coisas que queríamos tentar com esse doodle era quebrar a quarta parede. Com o mecanismo acima, pudemos combinar atores baseados em canvas com atores baseados em DOM. Por exemplo, as explosões no final estão em tela para objetos do universo e no DOM no restante da página inicial do Google. O pássaro, normalmente voando e cortado pela nossa máscara irregular, como qualquer outro ator, decide ficar longe de problemas durante a fase de tiro e senta no botão "Estou com sorte". Isso é feito para o pássaro sair da tela e se tornar um elemento DOM (e vice-versa depois), o que eu esperava ser completamente transparente para nossos visitantes.

O frame rate

Conhecer o frame rate atual e reagir quando ele está muito lento (e rápido demais) foi uma parte importante do nosso mecanismo. Como os navegadores não informam o frame rate, precisamos calculá-lo.

Comecei a usar o requestAnimationFrame, voltando para o setTimeout antigo, se o primeiro não estivesse disponível. O requestAnimationFrame economiza a CPU de maneira inteligente em algumas situações, embora façamos parte disso nós mesmos, como será explicado abaixo, mas também apenas nos permite ter um frame rate maior do que setTimeout.

Calcular o frame rate atual é simples, mas está sujeito a mudanças drásticas. Por exemplo, ele pode cair rapidamente quando outro aplicativo sobrecarrega o computador por um tempo. Portanto, calculamos um frame rate "rotativo" (média) apenas em cada 100 marcações físicas e tomamos decisões com base nisso.

Que tipo de decisões?

  • Se o frame rate for maior que 60 fps, ele será limitado. Atualmente, o requestAnimationFrame em algumas versões do Firefox não tem limite superior no frame rate, e não há motivo para desperdiçar a CPU. Observe que, na verdade, o limite é de 65 fps devido aos erros de arredondamento que tornam o frame rate um pouco maior que 60 fps em outros navegadores. Não queremos começar a limitar isso por engano.

  • Se o frame rate for menor que 10 QPS, vamos simplesmente reduzir o mecanismo desacelerando em vez de descartar frames. É uma proposta de perda de dados, mas achei que pular frames excessivamente seria mais confuso do que simplesmente ter um jogo mais lento (mas ainda coerente). Há outro efeito colateral bom disso: se o sistema ficar lento temporariamente, o usuário não vai passar por um salto estranho enquanto o motor está se recuperando desesperadamente. Fiz isso de uma maneira um pouco diferente no Pac-Man, mas o frame rate mínimo é uma abordagem melhor.

  • Por fim, podemos pensar em simplificar os gráficos quando o frame rate fica perigosamente baixo. Não estamos fazendo isso para o doodle do Lem, exceto o ponteiro do mouse (mais sobre isso abaixo), mas podemos perder algumas das animações irrelevantes para que o doodle pareça fluido mesmo em computadores mais lentos.

Também temos um conceito de um tique físico e um tick lógico. O primeiro vem de requestAnimationFrame/setTimeout. A proporção na jogabilidade normal é de 1:1, mas, para avançar, apenas adicionamos mais marcações lógicas por marcação física (até 1:5). Isso nos permite fazer todos os cálculos necessários para cada marcação lógica, mas designamos apenas o último para atualizar as coisas na tela.

Como fazer comparações

Podemos supor que, no início, a tela será mais rápida que o DOM quando estiver disponível. Isso nem sempre é verdade. Durante os testes, descobrimos que o Opera 10.0 a 10.1 em um Mac e o Firefox no Linux são, na verdade, mais rápidos ao mover elementos DOM.

No mundo ideal, o doodle comparava silenciosamente diferentes técnicas gráficas: elementos DOM movidos usando style.left e style.top, desenhando na tela e talvez até mesmo elementos DOM movidos usando transformações CSS3.

e depois alternar para a opção que tiver o maior frame rate. Comecei a escrever um código para isso, mas descobri que minha maneira de fazer comparações não era confiável e exigia muito tempo. Tempo que não temos na nossa página inicial: nós nos preocupamos muito com a velocidade e queremos que o doodle apareça instantaneamente e o jogo comece assim que você clicar ou tocar.

No final, o desenvolvimento da Web às vezes se resume a ter que fazer o que deve fazer. Olhei atrás do meu ombro para ter certeza de que ninguém estava olhando, e só codifiquei o Opera 10 e o Firefox no canvas. Na próxima vida, vou voltar como uma tag <marquee>.

Como conservar a CPU

Sabe aquele amigo que vem na sua casa, assiste ao final da temporada de Breaking Bad, estraga para você e depois exclui do DVR? Você não quer ser esse cara, quer?

Então, sim, a pior analogia de todas. Mas não queremos que nosso doodle seja esse cara. O fato de estarmos na 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 do mouse, movimentos do mouse ou pressionamento de tecla), queremos que ele entre no modo de suspensão.

Quando?

  • após 18 segundos na página inicial (jogos de arcade chamados de modo de atração)
  • após 180 segundos, se a guia estiver em foco
  • após 30 segundos se a guia não estiver em foco (por exemplo, o usuário mudou para outra janela, mas talvez ainda esteja vendo o doodle em uma guia inativa)
  • imediatamente se a guia ficar invisível, por exemplo, se o usuário alternou para outra guia na mesma janela, não há motivo para desperdiçar ciclos se não pudermos ser vistas.

Como sabemos que a guia está em foco no momento? Nós nos anexamos à window.focus e ao window.blur. Como podemos saber se a guia está visível? Estamos usando a nova API Page Visibility e reagimos ao evento apropriado.

Os tempos limite acima foram mais salientes do que o normal. Eu os adaptei para este doodle em particular, que tem muitas animações de ambiente (principalmente o céu e o pássaro). O ideal é que os tempos limite sejam limitados à interação no jogo. Por exemplo, logo após a queda, o pássaro pode informar ao doodle que ele pode ir dormir agora, mas não implementei isso no final.

Como o céu está sempre em movimento, ao adormecer e acordar o doodle não apenas para ou começa, ele desacelera antes de pausar e vice versa para retomar, aumentar ou diminuir o número de marcações lógicos por marcação física conforme necessário.

Transições, transformações, eventos

Uma das vantagens do HTML é sempre o fato de que você mesmo pode aprimorá-lo: se algo não for bom o suficiente no portfólio regular de HTML e CSS, use o JavaScript para estendê-lo. 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 ou usar transições para fazer nada além de estilizar elementos. Outro exemplo: as transformações CSS3 são ótimas para DOM, mas quando você passa para o canvas, de repente está sozinho.

Esses e outros problemas são o motivo pelo qual o doodle Lem tem o próprio mecanismo de transição e transformação. Sim, sei que os anos 2000 também chamaram etc. Os recursos que eu integramos não são tão poderosos quanto o CSS3, mas, seja qual for a ação do mecanismo, ele faz de maneira consistente e nos dá muito mais controle.

Comecei com um sistema de ação (eventos) simples: uma linha do tempo que dispara eventos no futuro sem usar setTimeout, já que o tempo do doodle pode ser divorciado do tempo físico conforme fica mais rápido (avanço), mais lento (baixa taxa de frames ou adormecendo para economizar CPU) ou para totalmente (aguardando o carregamento das imagens).

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

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

Como alguns objetos que giram estão anexados a outros objetos que estão em rotação, um exemplo é a mão de um robô conectada ao braço inferior, que está ligada a um braço em rotação, isso significa que eu também precisava criar a origem de transformação de um pobre homem na forma de pivôs.

Tudo isso é uma quantidade sólida de trabalho que, por fim, cobre o terreno cuidado do HTML5, mas às vezes o suporte nativo não é bom o suficiente e é hora da reinvenção da 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.

Sprite é uma técnica bem conhecida que também usamos para doodles. Isso nos permite economizar bytes e diminuir os tempos de carregamento, além de facilitar o pré-carregamento. No entanto, isso também dificulta o desenvolvimento, já que todas as mudanças nas imagens exigiriam uma nova combinação de sprites (bastante automatizada, mas ainda assim complicada). O mecanismo oferece suporte à execução em imagens brutas para desenvolvimento, bem como sprites para produção via engine.useSprites. Ambas estão incluídas no código-fonte.

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

Também aceitamos o pré-carregamento de imagens à medida que avançamos e interrompemos o doodle se as imagens não carregam a tempo, com uma barra de progresso falsa. Isso é um falso porque, infelizmente, nem mesmo o HTML5 pode informar quanto do arquivo de imagem já foi carregado.

Captura de tela do gráfico de carregamento com a barra de progresso manipulada.
Uma captura de tela do carregamento do gráfico com a barra de progresso manipulada.

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

Onde o HTML5 se encaixa nisso? Não há muito disso acima, mas a ferramenta que criei para sprites/recortes era uma nova tecnologia da Web: canvas, blobs, a[download]. Uma das coisas mais interessantes sobre o HTML é que ele inclui lentamente coisas que antes tinham que ser feitas fora do navegador. A única parte de que precisávamos foi a otimização dos arquivos PNG.

Salvar o estado entre jogos

Os mundos de Lem sempre foram grandes, vivos e realistas. Normalmente, as histórias dele começavam sem muita explicação, a primeira página começando com medias res, e o leitor tinha que encontrá-lo.

A Cyberiad não foi exceção, e queríamos replicar esse sentimento pelo doodle. Começamos tentando não explicar demais a história. Outra grande parte é a aleatoriedade, que parece adequada à natureza mecânica do universo do livro. Temos várias funções auxiliares que lidam com a aleatoriedade e usamos em diversos lugares.

Também queríamos aumentar a capacidade de jogar de outras maneiras. Para isso, precisávamos saber quantas vezes o doodle foi concluído antes. A solução tecnológica historicamente correta para isso é um cookie, mas que não funciona para a página inicial do Google: cada cookie aumenta o payload de cada página e, mais uma vez, nos preocupamos muito com a velocidade e a latência.

Felizmente, o HTML5 oferece um armazenamento da Web, que é trivial em uso, permitindo salvar e lembrar da contagem geral de peças e da última cena reproduzida pelo usuário, com muito mais graça do que os cookies jamais permitiriam.

O que fazemos com essas informações?

  • mostramos um botão de avanço, que permite percorrer as cenas que o usuário já viu antes
  • mostramos N itens diferentes durante o final
  • aumentamos um pouco a dificuldade do nível de tiro
  • mostramos um pequeno dragão de probabilidade easter egg de uma história diferente na terceira peça e nas próximas

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

  • ?doodle-debug&doodle-first-run – fingir que é a primeira execução
  • ?doodle-debug&doodle-second-run – finja que está na segunda corrida
  • ?doodle-debug&doodle-old-run – finge que é uma corrida antiga

Dispositivos por toque

Queríamos que o doodle parecesse confortável em dispositivos de toque. Os mais modernos são poderosos o suficiente para que funcionem muito bem, e experimentar o jogo por toque é muito mais divertido do que clicar.

Era necessário fazer algumas mudanças iniciais na experiência do usuário. Originalmente, o ponteiro do mouse era o único lugar que comunicava uma cena/parte não interativa. Mais tarde, adicionamos um pequeno indicador no canto inferior direito para que não fosse preciso depender apenas do ponteiro do mouse, já que ele não existe em dispositivos de toque.

Normal Alta procura Clicável Clicou em
Em desenvolvimento
Ponteiro normal de trabalho em andamento
Ponteiro ocupado do trabalho em andamento
Ponteiro clicável de trabalho em andamento
Ponteiro clicado em trabalho em andamento
Final
Ponteiro normal finalv
Ponteiro final ocupado
Ponteiro clicável final
Ponteiro final clicado
Ponteiros do mouse durante o desenvolvimento e equivalentes finais.

A maioria das coisas funcionou instantaneamente. No entanto, testes rápidos e não programados de usabilidade da nossa experiência de toque mostraram dois problemas: alguns dos destinos eram muito difíceis de pressionar e os toques rápidos foram ignorados porque apenas substituimos eventos de clique do mouse.

Ter elementos DOM transparentes clicáveis separados ajudou muito aqui, porque poderia redimensioná-los independentemente dos recursos visuais. Adicionei um padding extra de 15 pixels para dispositivos de toque 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, anexei e testei os gerenciadores de início e fim corretos de toque, 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 (toque em destaque, toque em frase de destaque).

E como detectamos se um determinado dispositivo que executa o doodle oferece suporte ao toque? Devagar. Em vez de descobrir uma priori, usamos nossos IQs combinados para deduzir que o dispositivo oferece suporte ao toque, depois do primeiro evento de início de toque.

Personalizar o ponteiro do mouse

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

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

Se não for isso, o quê? Por que não transformar o ponteiro do mouse como outro ator no doodle? Isso funciona, mas inclui várias ressalvas, principalmente:

  • será preciso conseguir 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 o uso de cursor: none, mas também não é compatível com alguns navegadores. Precisávamos recorrer a algumas ginásticas: usar o 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 como o ponteiro do mouse é apenas mais uma parte do universo do doodle, 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. Isso terá consequências graves, já que o ponteiro do mouse, sendo uma extensão natural da sua mão, precisa ser responsivo, a qualquer momento. As pessoas que usavam o Commodore Amiga no passado agora estão concordando com a cabeça vigorosamente.

Uma solução um pouco complexa para esse problema é desacoplar 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? Apenas reverter para o ponteiro do mouse nativo se a taxa de frames contínua cair para abaixo de 20 QPS. É aqui que o frame rate rolante é útil. Se reagássemos ao frame rate atual e se ela oscilasse em torno de 20 QPS, o usuário veria o ponteiro personalizado do mouse se escondendo e sendo exibido o tempo todo. Isso nos leva a:

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

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

Conclusão

Esse não é um mecanismo perfeito, mas não tenta ser assim. Ele foi desenvolvido junto com o doodle Lem e é muito específico para ele. Tudo bem. "A otimização prematura é a raiz de todo o mal", como disse Don Knuth, não acho que escrever um mecanismo isoladamente antes, e só aplicá-lo depois 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 repetidas vezes e muitas partes comuns foram observadas após o início, 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.

Espero que as informações acima esclareçam algumas das escolhas e compensações de design que precisávamos fazer e como usamos o HTML5 em um cenário específico da vida real. Agora, teste o código-fonte, faça uma experiência e nos conte o que você achou.

Eu mesmo fiz isso. O conteúdo abaixo foi publicado nos últimos dias, em contagem regressiva para as primeiras horas de 23 de novembro de 2011 na Rússia, que foi o primeiro fuso horário que viu o doodle do Lem. Isso pode ser divertido, mas, assim como os doodles, coisas que parecem insignificantes às vezes têm um significado mais profundo. Esse contador foi realmente um bom "teste de estresse" para o motor.

Captura de tela do doodle do Lem com um relógio de contagem regressiva no universo.
Captura de tela do doodle do Lem do relógio de contagem regressiva do universo.

Essa é uma maneira de encarar a vida de um doodle do Google: meses de trabalho, semanas de testes, 48 horas de preparo, tudo por algo que as pessoas jogam por cinco minutos. Cada uma dessas milhares de linhas JavaScript espera que esses cinco minutos sejam bem gastos. Aproveite.