Caso de éxito: Creación del doodle Stanisław Lem de Google

Hola, mundo (extraño)

La página de inicio de Google es un entorno fascinante para programar. Además, incluye muchas restricciones desafiantes: un enfoque particular en la velocidad y la latencia, tener que brindar servicios a todo tipo de navegadores y trabajar en varias circunstancias, y... sí, sorprender y deleitar.

Me refiero a los garabatos de Google, las ilustraciones especiales que a veces reemplazan nuestro logotipo. Aunque mi relación con los bolígrafos y los pinceles ha tenido durante mucho tiempo el sabor distintivo de un orden restringido, a menudo, contribuyo con los elementos interactivos.

Todos los doodles interactivos que codifiqué (Pac-Man, Jules Verne, Feria Mundial), y muchos con los que ayudé, fueron en partes iguales futuristas y anacrónicos: grandes oportunidades para las aplicaciones panorámicas de las funciones web de vanguardia... y el firme pragmatismo de la compatibilidad entre navegadores.

Aprendemos mucho de cada doodle interactivo, y el reciente minijuego Stanisław Lem no fue la excepción, con sus 17,000 líneas de código JavaScript que prueban muchas cosas por primera vez en la historia de los doodles. Hoy, quiero compartir ese código contigo (tal vez encuentres algo interesante allí o señalar mis errores) y hablar un poco al respecto.

Ver el código del doodle de Stanisław Lem »

Algo que vale la pena tener en cuenta es que la página principal de Google no es un lugar para demostraciones tecnológicas. Con nuestros dibujos, queremos celebrar a personas y eventos específicos, y lo hacemos con las mejores técnicas y el mejor arte que podamos convocar, pero nunca celebraremos la tecnología por el bien de la tecnología. Esto significa mirar detenidamente cualquier parte disponible de HTML5 ampliamente comprendido y ver si nos ayuda a mejorar el doodle sin distraerlo ni eclipsarlo.

Entonces, analicemos algunas de las tecnologías web modernas que encontraron su lugar, y otras que no lo hicieron, en el doodle de Stanisław Lem.

Gráficos mediante DOM y lienzo

Canvas es potente y se creó para exactamente el tipo de tareas que queríamos hacer en este doodle. Sin embargo, algunos de los navegadores más antiguos que nos interesaban no eran compatibles y, aunque literalmente comparto una oficina con la persona que creó un excanvas excelente, decidí elegir otra forma.

Armé un motor gráfico que abstrae las primitivas gráficas llamadas "rects" y, luego, los renderiza con lienzos o DOM si el lienzo no está disponible.

Este enfoque conlleva algunos desafíos interesantes, por ejemplo, mover o cambiar un objeto en DOM tiene consecuencias inmediatas, mientras que, en el caso del lienzo, hay un momento específico en el que todo se dibuja al mismo tiempo. (decidí tener solo un lienzo, borrarlo y dibujar desde cero con cada fotograma. Hay demasiadas partes móviles, literalmente, en una mano y, por la otra, no tienen la complejidad suficiente como para justificar la división en varios lienzos superpuestos y actualizarlos de forma selectiva).

Lamentablemente, cambiar al lienzo no es tan simple como duplicar fondos CSS con drawImage(): pierdes varias funciones gratuitas cuando se combinan mediante DOM, sobre todo las capas con índices z y eventos del mouse.

Ya abstraé el índice z con un concepto llamado "planos". El doodle definió una cantidad de planos (desde el cielo muy atrás hasta el puntero del mouse que está frente a todo) y cada actor del doodle tenía que decidir a cuál pertenecía (las pequeñas correcciones de signo más o menos dentro de un plano eran posibles con planeCorrection).

Cuando se procesa a través del DOM, los planos simplemente se traducen al índice z. Sin embargo, si renderizamos mediante lienzo, necesitamos ordenar los rectángulos en función de sus planos antes de dibujarlos. Dado que es costoso hacerlo cada vez, el pedido se vuelve a calcular solo cuando se agrega un actor o cuando se mueve a otro plano.

También lo abstraté para los eventos de mouse... algo así. Tanto para el DOM como para el lienzo, utilicé elementos del DOM flotantes adicionales y completamente transparentes con un índice z alto, cuya función solo es para reaccionar cuando se desplaza el mouse por encima o se aleja, se hace clic y se presiona.

Una de las cosas que queríamos probar con este doodle era romper la cuarta pared. El motor anterior nos permitió combinar actores basados en lienzos con actores basados en DOM. Por ejemplo, las explosiones del final están en lienzo para objetos del universo y en DOM para el resto de la página principal de Google. El pájaro, que normalmente vuela y cortado por nuestra máscara dentada como cualquier otro actor, decide mantenerse fuera de problemas durante el nivel de tiro y se sienta en el botón Voy a tener suerte. Así, el ave sale del lienzo y se convierte en un elemento del DOM (y viceversa más adelante), algo que esperaba que fuera completamente transparente para nuestros visitantes.

La velocidad de fotogramas

Conocer la velocidad de fotogramas actual y reaccionar cuando es demasiado lento (y demasiado rápido) fue una parte importante de nuestro motor. Dado que los navegadores no informan la velocidad de fotogramas, tenemos que calcularla nosotros mismos.

Comencé a usar requestAnimationFrame y retomaba el setTimeout antiguo si el primero no estaba disponible. requestAnimationFrame guarda la CPU de forma inteligente en algunas situaciones (aunque nosotros nos encargamos de eso, como se explicará a continuación), pero también simplemente nos permite obtener una velocidad de fotogramas más alta que setTimeout.

Calcular la velocidad de fotogramas actual es simple, pero está sujeto a cambios drásticos; por ejemplo, puede disminuir rápidamente cuando otra aplicación acapara la computadora durante un tiempo. Por lo tanto, calculamos una velocidad de fotogramas "promediada" (promediada) en cada 100 marcas físicas y tomamos decisiones basadas en eso.

¿Qué tipo de decisiones?

  • Si la velocidad de fotogramas es superior a 60 fps, la regulamos. Actualmente, requestAnimationFrame en algunas versiones de Firefox no tiene límite superior en la velocidad de fotogramas, y no tiene sentido desperdiciar la CPU. Ten en cuenta que, en realidad, el límite es de 65 FPS debido a los errores de redondeo que hacen que la velocidad de fotogramas sea un poco superior a 60 FPS en otros navegadores. No es recomendable comenzar a limitar esa velocidad por error.

  • Si la velocidad de fotogramas es inferior a 10 FPS, simplemente ralentizamos el motor en lugar de descartarlos. Es una propuesta de perder o perder, pero sentí que omitir fotogramas iba a ser demasiado más confuso que simplemente tener un juego más lento (pero coherente). Hay otro efecto secundario atractivo: si el sistema se vuelve lento temporalmente, el usuario no experimentará un salto extraño, ya que el motor se está poniendo al día desesperadamente. (Lo hice de manera un poco diferente para Pac-Man, pero la velocidad de fotogramas mínima es un mejor enfoque).

  • Por último, se puede simplificar los gráficos cuando la velocidad de fotogramas está peligrosamente baja. No lo hacemos para el doodle de Lem, a excepción del puntero del mouse (más información a continuación), pero, hipotéticamente, podríamos perder algunas de las animaciones extrañas solo para que el doodle se sienta fluido incluso en computadoras más lentas.

También tenemos un concepto de una marca física y una marca lógica. El primero proviene de requestAnimationFrame y setTimeout. La proporción en un juego normal es de 1:1, pero para adelantar el contenido solo agregamos más marcas lógicas por cada marca física (hasta 1:5). Esto nos permite hacer todos los cálculos necesarios para cada marca lógica, pero solo designar el último para que sea el que actualice los elementos en la pantalla.

Comparativas

Se puede suponer (y, de hecho, fue, al principio) que el lienzo será más rápido que el DOM cuando esté disponible. No siempre es así. Durante las pruebas, descubrimos que Opera 10.0 a 10.1 en Mac y Firefox en Linux son más rápidos cuando se mueven elementos del DOM.

En el mundo perfecto, el doodle comparaba silenciosamente diferentes técnicas gráficas: los elementos del DOM se movían con style.left y style.top, dibujaban en lienzo e incluso los elementos del DOM se movían con transformaciones CSS3.

y, luego, cambiar al que proporcione la velocidad de fotogramas más alta. Comencé a escribir código para eso, pero descubrí que al menos mi forma de generar comparativas era bastante poco confiable y requería mucho tiempo. Tiempo que no tenemos en nuestra página principal: nos importa mucho la velocidad y queremos que el doodle aparezca al instante y que el juego comience en cuanto hagas clic o presiones.

Al final, el desarrollo web a veces se reduce a tener que hacer lo que tienes que hacer. Miré detrás de mí para asegurarme de que nadie estuviera mirando y, luego, codifiqué Opera 10 y Firefox fuera de lienzo. En la próxima vida, volveré como una etiqueta <marquee>.

Conserva la CPU

¿Conoces a ese amigo que viene a tu casa, mira el final de temporada de Breaking Bad, lo arruina por ti y, luego, lo borra de tu DVR? No quieres ser ese tipo, ¿verdad?

Sí, la peor analogía de la historia. Pero no queremos que nuestro doodle sea ese tipo de persona: el hecho de tener permiso en la pestaña del navegador de alguien es un privilegio, y acumular ciclos de CPU o distraer al usuario nos convertiría en un invitado desagradable. Por lo tanto, si nadie juega con el doodle (sin toques, clics del mouse, movimientos del mouse o pulsaciones de teclas), queremos que finalmente se suspenda.

¿Cuándo?

  • Después de 18 segundos en la página principal (los juegos de arcade lo llaman modo de atracción)
  • Después de 180 segundos, si la pestaña está enfocada
  • después de 30 segundos si la pestaña no está enfocada (p.ej., el usuario cambió a otra ventana, pero tal vez todavía está mirando el doodle en una pestaña inactiva)
  • inmediatamente si la pestaña se vuelve invisible (p.ej., el usuario cambió a otra pestaña en la misma ventana; no tiene ningún punto en desperdicio de ciclos si no podemos vernos)

¿Cómo sabemos que la pestaña está enfocada? Nos adjuntamos a window.focus y window.blur. ¿Cómo sabemos que la pestaña es visible? Estamos usando la nueva API de visibilidad de la página y reaccionamos ante el evento correspondiente.

Los tiempos de espera anteriores nos permiten perdonar más de lo habitual. Las adapté a este doodle en particular, que tiene muchas animaciones ambientales (principalmente el cielo y el pájaro). Idealmente, los tiempos de espera estarán restringidos a la interacción en el juego, p.ej., justo después del aterrizaje, el ave podría informar al doodle que puede dormirse ahora, pero no lo implementé al final.

Dado que el cielo está siempre en movimiento, al dormirse y al despertar, el doodle no solo se detiene o comienza; se ralentiza antes de pausar y viceversa para reanudar, aumentar o disminuir el número lógico de marcas por una marca física, según sea necesario.

Transiciones, transformaciones y eventos

Uno de los beneficios del HTML siempre ha sido el hecho de que puedes mejorarlo tú mismo: si algo no es lo suficientemente bueno en la cartera habitual de HTML y CSS, puedes derivar JavaScript para extenderlo. Lamentablemente, a menudo significa tener que empezar desde cero. Las transiciones CSS3 son excelentes, pero no puedes agregar un nuevo tipo de transición ni usar transiciones para realizar acciones que no sean aplicar diseño a los elementos. Otro ejemplo: las transformaciones CSS3 son excelentes para el DOM, pero cuando pasas al lienzo, de repente estás solo.

Estos problemas, entre otros, son la razón por la que el doodle de Lem tiene su propio motor de transformación y transición. Sí, lo sé, llamada a los años 2000, etc. Las funciones que incorporé no son tan potentes como CSS3, pero sea lo que sea que haga el motor, funciona de manera coherente y nos da mucho más control.

Comencé con un sistema simple de acción (evento): un cronograma que activa eventos en el futuro sin usar setTimeout, ya que en cualquier momento del tiempo del doodle puede separarse del tiempo físico a medida que se hace más rápido (adelante), más lento (velocidad de fotogramas baja o quedarse dormido para ahorrar CPU) o se detiene por completo (esperando que las imágenes terminen de cargarse).

Las transiciones son otro tipo de acciones. Además de los movimientos y la rotación básicos, también admitimos movimientos relativos (p.ej., mover algo 10 píxeles hacia la derecha), elementos personalizados como temblores y animaciones de imágenes de fotogramas clave.

Mencioné las rotaciones, y estas también se hacen manualmente: tenemos objetos para varios ángulos para los objetos que se deben rotar. La razón principal es que tanto las rotaciones CSS3 como las de lienzo introdujeron artefactos visuales que consideramos inaceptables y, además, esos artefactos variaron según la plataforma.

Dado que algunos objetos que rotan están unidos a otros objetos giratorios, un ejemplo es la mano de un robot conectada al brazo inferior, que en sí está unida a un brazo superior giratorio, esto significaba que también necesitaba crear el origen de transformación de un pobre en forma de pivotes.

Todo esto es una cantidad sólida de trabajo que, en última instancia, cubre el terreno que ya se ocupa de HTML5, pero a veces la compatibilidad nativa no es lo suficientemente buena y es el momento de reinventar la rueda.

Cómo trabajar con imágenes y objetos

Un motor no solo sirve para ejecutar el doodle, sino también para trabajar en él. Compartí algunos parámetros de depuración anteriormente: puedes encontrar el resto en engine.readDebugParams.

Spriting es una técnica conocida que también usamos para dibujar garabatos. Nos permite ahorrar bytes y disminuir los tiempos de carga. Además, nos facilita la precarga. Sin embargo, también dificulta el desarrollo, ya que cada cambio en las imágenes requeriría volver a crear un objeto (un proceso en gran medida automatizado, pero engorroso de todos modos). Por lo tanto, el motor admite la ejecución en imágenes sin procesar para el desarrollo y en objetos para la producción a través de engine.useSprites. Ambos están incluidos con el código fuente.

Doodle de Pac-Man
Objetos utilizados por el doodle de Pac-Man

También admitimos la carga previa de imágenes a medida que avanzamos y la detención del doodle si las imágenes no se cargaban a tiempo, ¡y agregamos una barra de progreso falsa! (Es falso porque, lamentablemente, ni siquiera HTML5 puede decirnos cuánto se cargó un archivo de imagen).

Captura de pantalla del gráfico de carga con la barra de progreso ajustada.
Captura de pantalla del gráfico de carga con la barra de progreso ajustada.

En algunas escenas, no usamos más de un objeto para acelerar la carga con conexiones paralelas, sino simplemente debido a la limitación de 3/5 millones de píxeles para imágenes en iOS.

¿Dónde encaja el HTML5 en todo esto? No hay mucho de eso arriba, pero la herramienta que escribí para crear un objeto o recortar era una nueva tecnología web: lienzos, BLOB y a[download]. Uno de los beneficios más interesantes del HTML es que, de a poco, completa las tareas que antes se debían hacer fuera del navegador. Lo único que necesitábamos hacer era optimizar los archivos PNG.

Cómo guardar el estado entre juegos

Los mundos de Lem siempre se sentían grandes, vivos y realistas. Por lo general, sus historias comenzaban sin mucha explicación, la primera página empezaba en medias res, y el lector tenía que buscarla.

Cyberiad no fue la excepción y queríamos replicar ese sentimiento por el doodle. Empezamos tratando de no explicar la historia en exceso. Otra parte importante es la aleatorización, que creemos que se adecua a la naturaleza mecánica del universo del libro; tenemos una serie de funciones auxiliares que se ocupan de la aleatorización y que usamos en muchos lugares.

También queríamos aumentar la posibilidad de volver a jugar de otras maneras. Para eso, necesitábamos saber cuántas veces había terminado el doodle antes. La solución tecnológica históricamente correcta es una cookie, pero eso no funciona en la página principal de Google: cada cookie aumenta la carga útil de cada página y, de nuevo, nos preocupamos mucho por la velocidad y la latencia.

Afortunadamente, HTML5 nos proporciona Web Storage, un uso trivial, que nos permite guardar y recuperar el recuento general de reproducciones y la última escena que reprodujo el usuario, con mucha más gracia de la que permitirían las cookies.

¿Qué hacemos con esta información?

  • mostramos un botón de avance rápido que permite recorrer las escenas cinemáticas que el usuario ya vio antes.
  • mostramos diferentes N elementos durante el final
  • aumentamos ligeramente la dificultad del nivel de tiro
  • mostramos una pequeña probabilidad de huevo de pascua de dragón de una historia diferente en la tercera jugada y las subsiguientes.

Existen varios parámetros de depuración que controlan esto:

  • ?doodle-debug&doodle-first-run: simula que es una primera carrera.
  • ?doodle-debug&doodle-second-run: simula que es una segunda carrera.
  • ?doodle-debug&doodle-old-run: simula que es una antigua carrera.

Dispositivos táctiles

Queríamos que el doodle se sintiera como en casa en dispositivos táctiles: los más modernos son lo suficientemente potentes como para que el doodle funcione muy bien, y experimentar el juego con la función de presionar es mucho más divertido que hacer clic.

Era necesario realizar algunos cambios iniciales en la experiencia del usuario. Originalmente, el puntero del mouse era el único lugar en el que se comunicaba una escena de corte o parte no interactiva. Luego, agregamos un pequeño indicador en la esquina inferior derecha para no depender solo del puntero del mouse (dado que no se encuentra en los dispositivos táctiles).

Normal Ocupado En el que se puede hacer clic Hiciste clic en
Trabajo en curso
Puntero normal de trabajo en curso
Puntero ocupado del trabajo en curso
Puntero de trabajo en curso en el que se puede hacer clic
Puntero de trabajo en curso en el que se hizo clic
Final
Puntero normal finalv
Puntero final ocupado
Puntero final en el que se puede hacer clic
Puntero en el que se hizo clic final
Punteros del mouse durante el desarrollo y equivalentes finales.

La mayoría de las cosas funcionó de inmediato. Sin embargo, las pruebas improvisadas y rápidas de usabilidad de nuestra experiencia táctil mostraron dos problemas: algunos de los objetivos eran demasiado difíciles de presionar y se ignoraron los toques rápidos, ya que solo anulamos los eventos de clic del mouse.

Tener elementos separados del DOM transparentes en los que se puede hacer clic ayudó mucho aquí, ya que podía cambiar su tamaño independientemente de los elementos visuales. Presenté un padding adicional de 15 píxeles para los dispositivos táctiles y lo usé cada vez que se creaban elementos en los que se podía hacer clic. También agregué un padding de 5 píxeles para el mouse, solo para satisfacer al Sr. Fitts.

En cuanto al otro problema, me aseguré de conectar y probar los controladores táctiles de inicio y finalización táctiles, en lugar de depender del clic del mouse.

También usamos propiedades de estilo más modernas para quitar algunas funciones táctiles que los navegadores WebKit agregan de forma predeterminada (p. ej., para destacar texto, presionar el texto destacado).

¿Y cómo detectamos si un dispositivo determinado que ejecuta el doodle admite el tacto? Con lentitud. En lugar de averiguar un orden de prioridad, solo usamos nuestros coeficientes intelectuales combinados para deducir que el dispositivo es compatible con el tacto... después de obtener el primer evento de inicio táctil.

Cómo personalizar el puntero del mouse

Pero no todo se basa en el tacto. Uno de nuestros principios rectores era incluir todo lo que se pudiera en el universo del doodle. La IU pequeña de la barra lateral (adelante, signo de interrogación), la información sobre la herramienta y hasta, sí, el puntero del mouse.

¿Cómo puedo personalizar un puntero del mouse? Algunos navegadores permiten cambiar el cursor del mouse mediante la vinculación a un archivo de imagen personalizado. Sin embargo, no se admite bien y es algo restrictivo.

Si no es así, ¿qué sucede? Bueno, ¿por qué no hacer que el puntero del mouse sea solo otro actor del doodle? Esto funciona, pero hay algunas advertencias, sobre todo:

  • debes poder quitar el puntero nativo del mouse
  • debes ser muy bueno para mantener el puntero del mouse sincronizado con el "real"

La primera es complicada. CSS3 permite cursor: none, pero tampoco es compatible con algunos navegadores. Tuvimos que recurrir a la gimnasia: usar un archivo .cur vacío como resguardo, especificar un comportamiento concreto para algunos navegadores e incluso codificar otros fuera de la experiencia.

La otra es relativamente trivial, pero como el puntero del mouse es simplemente otra parte del universo del doodle, también heredará todos sus problemas. ¿El más grande? Si la velocidad de fotogramas del doodle es baja, la del puntero del mouse también lo será, lo que tiene consecuencias graves, ya que el puntero del mouse, que es una extensión natural de la mano, debe adaptarse sin importar lo que suceda. (Las personas que usaron Commodore Amiga en el pasado ahora asienten con energía).

Una solución un poco compleja a ese problema es separar el puntero del mouse del bucle de actualización normal. Eso lo hicimos, en un universo alternativo donde no necesito dormir. ¿Una solución más simple para esta solución? Solo se revierte al puntero nativo del mouse si la velocidad de fotogramas continua cae por debajo de los 20 FPS. (Aquí es donde resulta útil la velocidad de fotogramas progresivas. Si reaccionáramos a la velocidad de fotogramas actual y oscilara a unos 20 FPS, el usuario vería que el puntero del mouse personalizado se oculta y se muestra todo el tiempo). Esto nos lleva a:

Rango de velocidad de fotogramas Comportamiento
>10fps Reduce la velocidad del juego para que no se pierdan más fotogramas.
De 10 a 20 fps Usa el puntero del mouse nativo en lugar de uno personalizado.
20 a 60 fps Funcionamiento normal.
>60fps Acelera para que la velocidad de fotogramas no supere este valor.
Resumen del comportamiento que depende de la velocidad de fotogramas.

Y el puntero del mouse es oscuro en una Mac, pero blanco en una PC. ¿Por qué? Porque las guerras de plataformas necesitan combustible incluso en universos ficticios.

Conclusión

Este no es un motor perfecto, pero no intenta serlo. Se desarrolló junto con el doodle de Lem y es muy específico para él. Está bien. "La optimización prematura es la raíz del mal", como dijo Don Knuth. No creo que escribir un motor de forma aislada primero y aplicarla después tenga sentido: la práctica informa tanto la teoría como la práctica. En mi caso, el código se desechó, varias partes reescritas una y otra vez, y muchas piezas comunes se observaban publicadas, en lugar de ante hechos. Pero, al final, lo que nosotros nos permitió hacer lo que queríamos: celebrar la carrera de Stanisław Lem y los dibujos de Daniel Mróz de la mejor manera que se nos pueda imaginar.

Espero que lo anterior aclare algunas de las concesiones y elecciones de diseño que debemos hacer, y cómo usamos HTML5 en una situación específica de la vida real. Ahora, prueba el código fuente, pruébalo y danos tu opinión.

Lo hice yo mismo. A continuación, se publicó en los últimos días, hasta las primeras horas del 23 de noviembre de 2011 en Rusia, que fue la primera zona horaria en la que se vio el doodle de Lem. Es un tema ridículo, tal vez, pero al igual que los garabatos, los elementos que parecen insignificantes a veces tienen un significado más profundo. Este contador fue realmente una buena "prueba de esfuerzo" para el motor.

Captura de pantalla del Reloj de cuenta regresiva del doodle de Lem en el universo
Captura de pantalla del reloj de cuenta regresiva del doodle de Lem en el universo

Esa es una forma de ver la vida de un doodle de Google: meses de trabajo, semanas de pruebas, 48 horas de preparación, todo por algo que las personas juegan durante cinco minutos. Cada una de esas miles de líneas de JavaScript espera que esos 5 minutos sean tiempo suficiente. Que lo disfruten.