Hola, (extraño) mundo
La página principal de Google es un entorno fascinante para escribir código. Tiene muchas restricciones desafiantes: un enfoque particular en la velocidad y la latencia, tener que satisfacer todo tipo de navegadores y trabajar en varias circunstancias, y… sí, sorprender y deleitar.
Me refiero a los doodles de Google, las ilustraciones especiales que, en ocasiones, reemplazan nuestro logotipo. Y, si bien mi relación con los lápices y los pinceles tiene desde hace mucho ese sabor distintivo de una orden de restricción, a menudo contribuyo con los interactivos.
Cada garabato interactivo que codifiqué (Pac-Man, Jules Verne, Feria Mundial) y muchos de los que ayudé a crear eran en partes iguales futuristas y anacrónicos: grandes oportunidades para aplicaciones fantásticas de funciones web de vanguardia… y un pragmatismo implacable de compatibilidad con varios navegadores.
Aprendemos mucho de cada doodle interactivo, y el minijuego reciente de Stanisław Lem no fue la excepción, con sus 17,000 líneas de código JavaScript que probaron muchas cosas por primera vez en la historia de los doodles. Hoy quiero compartir ese código contigo. Quizás encuentres algo interesante o me indiques mis errores. También quiero hablar un poco sobre él.
Ver el código del doodle de Stanisław Lem »
Es importante tener en cuenta que la página principal de Google no es un lugar para demos de tecnología. Con nuestros doodles, queremos celebrar a personas y eventos específicos, y queremos hacerlo con el mejor arte y las mejores tecnologías que podamos reunir, pero nunca celebramos la tecnología por sí misma. Esto significa que debemos analizar cuidadosamente cualquier parte del HTML5 ampliamente entendido que esté disponible y si nos ayuda a mejorar el garabato sin distraerlo ni eclipsarlo.
Así que, analicemos algunas de las tecnologías web modernas que se incluyeron en el doodle de Stanisław Lem y otras que no.
Gráficos a través del DOM y el lienzo
Canvas es potente y se creó exactamente para el tipo de cosas que queríamos hacer en este garabato. Sin embargo, algunos de los navegadores más antiguos que nos interesaban no lo admitían, 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 “rectángulos” y, luego, las renderiza con el lienzo o el DOM si el lienzo no está disponible.
Este enfoque presenta algunos desafíos interesantes; por ejemplo, mover o cambiar un objeto en el DOM tiene consecuencias inmediatas, mientras que, para el lienzo, hay un momento específico en el que todo se dibuja al mismo tiempo. (Decidí tener un solo lienzo, borrarlo y dibujar desde cero con cada fotograma. Hay demasiadas partes móviles, literalmente, por un lado, y, por otro, no hay suficiente complejidad para justificar la división en varios lienzos superpuestos y actualizarlos de forma selectiva.
Lamentablemente, cambiar a Canvas no es tan simple como duplicar los fondos de CSS con drawImage()
: pierdes una serie de elementos que se incluyen de forma gratuita cuando se unen a través del DOM, lo más importante son las capas con índices z y los eventos del mouse.
Ya abstraje el índice z con un concepto llamado “planos”. El garabato definió una serie de planos, desde el cielo en la parte posterior hasta el puntero del mouse frente a todo, y cada actor dentro del garabato tuvo que decidir a cuál pertenecía (se podían hacer pequeñas correcciones de más o menos dentro de un plano con planeCorrection
).
Cuando se renderizan a través del DOM, los planos simplemente se traducen en el índice z. Sin embargo, si renderizamos a través del lienzo, debemos ordenar los rectángulos según sus planos antes de dibujarlos. Dado que es costoso hacerlo cada vez, el orden se vuelve a calcular solo cuando se agrega un actor o cuando se mueve a otro plano.
Para los eventos del mouse, también lo abstraje… en cierto modo. Para el DOM y el lienzo, usé elementos DOM flotantes completamente transparentes adicionales con un índice z alto, cuya función es solo reaccionar al desplazamiento del mouse sobre o fuera de ellos, a los clics y a las presiones.
Una de las cosas que queríamos probar con este garabato era romper la cuarta pared. El motor anterior nos permitió combinar actores basados en el lienzo con actores basados en el DOM. Por ejemplo, las explosiones del final están en el lienzo para los objetos del universo y en el DOM para el resto de la página principal de Google. El pájaro, que normalmente vuela y se recorta con nuestra máscara irregular como cualquier otro actor, decide no meterse en problemas durante el nivel de disparo y se sienta en el botón I’m Feeling Lucky. La forma en que se hace es que el pájaro salga del lienzo y se convierta en un elemento DOM (y viceversa más adelante), lo que esperaba que fuera completamente transparente para nuestros visitantes.
La velocidad de fotogramas
Conocer la velocidad de fotogramas actual y reaccionar cuando es demasiado lenta (o demasiado rápida) fue una parte importante de nuestro motor. Dado que los navegadores no informan la velocidad de fotogramas, debemos calcularla nosotros mismos.
Comencé a usar requestAnimationFrame y, luego, volví a la setTimeout
tradicional si la primera no estaba disponible.
requestAnimationFrame
ahorra la CPU de manera inteligente en algunas situaciones (aunque hacemos algo de eso nosotros mismos, como se explicará a continuación), pero también nos permite obtener una velocidad de fotogramas más alta que setTimeout
.
Calcular la velocidad de fotogramas actual es sencillo, pero está sujeto a cambios drásticos; por ejemplo, puede disminuir rápidamente cuando otra aplicación monopoliza la computadora durante un tiempo. Por lo tanto, calculamos una velocidad de fotogramas “continuada” (promedio) solo en cada 100 marcas físicas y tomamos decisiones en función de eso.
¿Qué tipo de decisiones?
Si la velocidad de fotogramas es superior a 60 fps, la limitamos. Actualmente,
requestAnimationFrame
en algunas versiones de Firefox no tiene un límite superior en la velocidad de fotogramas, y no tiene sentido desperdiciar la CPU. Ten en cuenta que, en realidad, limitamos a 65 fps debido a los errores de redondeo que hacen que la velocidad de fotogramas sea un poco más alta que 60 fps en otros navegadores. No queremos comenzar a reducirla por error.Si la velocidad de fotogramas es inferior a 10 fps, simplemente ralentizamos el motor en lugar de descartar fotogramas. Es una propuesta en la que no se gana nada, pero sentí que omitir fotogramas de forma excesiva sería más confuso que simplemente tener un juego más lento (pero aún coherente). Hay otro efecto secundario positivo: si el sistema se ralentiza temporalmente, el usuario no experimentará un salto extraño mientras el motor se pone al día desesperadamente. (Lo hice de manera ligeramente diferente para Pac-Man, pero la velocidad de fotogramas mínima es un mejor enfoque).
Por último, podemos pensar en simplificar los gráficos cuando la velocidad de fotogramas es demasiado baja. No lo hacemos para el garabato de Lem, a excepción del puntero del mouse (hablamos de esto más adelante), pero, hipotéticamente, podríamos perder algunas de las animaciones superfluas para que el garabato se sienta fluido incluso en computadoras más lentas.
También tenemos el concepto de un tick físico y un tick lógico. El primero proviene de requestAnimationFrame
/setTimeout
. La proporción en el juego normal es de 1:1, pero para el avance rápido, solo agregamos más marcas lógicas por marca física (hasta 1:5). Esto nos permite hacer todos los cálculos necesarios para cada marca lógica, pero solo designamos el último para que sea el que actualice los elementos en la pantalla.
Comparativas
Se puede suponer (y, de hecho, al principio, se hizo) que el lienzo será más rápido que el DOM siempre que esté disponible. No siempre es así. Durante las pruebas, descubrimos que Opera 10.0–10.1 en Mac y Firefox en Linux son más rápidos cuando se mueven elementos DOM.
En un mundo ideal, el garabato compararía en silencio diferentes técnicas gráficas: elementos del DOM que se mueven con style.left
y style.top
, dibujo en lienzo y, tal vez, incluso elementos del DOM que se mueven con transformaciones CSS3.
y, luego, cambia a la que tenga la velocidad de fotogramas más alta. Comencé a escribir código para eso, pero descubrí que, al menos, mi forma de realizar 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 garabato aparezca de inmediato 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 se debe hacer. Miré detrás de mí para asegurarme de que nadie estuviera mirando y, luego, codifiqué de forma fija Opera 10 y Firefox fuera del lienzo. En la próxima vida, volveré como una etiqueta <marquee>
.
Cómo conservar la CPU
¿Conoces a ese amigo que viene a tu casa, mira el final de la temporada de Breaking Bad, te lo cuenta y, luego, lo borra de tu DVR? No quieres ser ese tipo, ¿verdad?
Así que, sí, es la peor analogía que se haya hecho. Pero tampoco queremos que nuestro garabato sea así. El hecho de que se nos permita ingresar a la pestaña del navegador de alguien es un privilegio, y acaparar ciclos de CPU o distraer al usuario nos convertiría en un invitado desagradable. Por lo tanto, si nadie está jugando con el garabato (sin toques, clics del mouse, movimientos del mouse ni pulsaciones de teclas), queremos que, en algún momento, se apague.
¿Cuándo?
- después de 18 segundos en la página principal (los juegos de arcade lo llamaban modo atracción)
- después de 180 segundos si la pestaña tiene el foco
- después de 30 segundos si la pestaña no está enfocada (p.ej., el usuario cambió a otra ventana, pero quizás aún está mirando el garabato 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 sentido desperdiciar ciclos si no se puede ver).
¿Cómo sabemos que la pestaña tiene el enfoque en este momento? Nos adjuntamos a window.focus
y window.blur
. ¿Cómo sabemos que la pestaña es visible? Usamos la nueva API de Page Visibility y reaccionamos al evento adecuado.
Los tiempos de espera anteriores son más tolerantes de lo habitual. Los adapté a este garabato en particular, que tiene muchas animaciones ambientales (principalmente el cielo y el pájaro). Idealmente, los tiempos de espera se limitarían en la interacción en el juego (p.ej., justo después de aterrizar, el pájaro podría informarle al garabato que puede irse a dormir ahora), pero no lo implementé al final.
Como el cielo siempre está en movimiento, cuando se duerme y se despierta, el garabato no solo se detiene o comienza, sino que se ralentiza antes de detenerse y viceversa para reanudar, aumentar o disminuir la cantidad de marcas lógicas por marca física según sea necesario.
Transiciones, transformaciones y eventos
Uno de los poderes del HTML siempre ha sido el hecho de que puedes mejorarlo tu mismo: si algo no es lo suficientemente bueno en el portafolio normal de HTML y CSS, puedes usar JavaScript para extenderlo. Lamentablemente, a menudo significa tener que empezar desde cero. Las transiciones de CSS3 son excelentes, pero no puedes agregar un tipo de transición nuevo ni usar transiciones para hacer nada más que aplicar diseño a los elementos. Otro ejemplo: las transformaciones de CSS3 son excelentes para el DOM, pero cuando te mueves al lienzo, de repente te encuentras solo.
Estos problemas y muchos más son la razón por la que el garabato de Lem tiene su propio motor de transición y transformación. Sí, lo sé, los años 2000 llamaron, etc. Las funciones que incorporé no son tan potentes como CSS3, pero lo que hace el motor lo hace de forma coherente y nos brinda mucho más control.
Comencé con un sistema de acciones (eventos) simple: un cronograma que activa eventos en el futuro sin usar setTimeout
, ya que, en cualquier momento, el tiempo de garabato puede separarse del tiempo físico a medida que se acelera (adelante rápido), se ralentiza (baja velocidad de fotogramas o se queda dormido para ahorrar CPU) o se detiene por completo (espera a que las imágenes terminen de cargarse).
Las transiciones son solo 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 a la derecha), elementos personalizados como el parpadeo y animaciones de imágenes de fotogramas clave.
Mencioné las rotaciones, que también se realizan de forma manual: tenemos sprites para varios ángulos de los objetos que se deben rotar. El motivo principal es que las rotaciones de CSS3 y lienzo introducían artefactos visuales que considerábamos inaceptables. Además, esos artefactos variaban según la plataforma.
Dado que algunos objetos que rotan están conectados a otros objetos que rotan (un ejemplo es la mano de un robot conectada al brazo inferior, que a su vez está conectada a un brazo superior giratorio), también tuve que crear un origen de transformación de bajo costo en forma de pivotes.
Todo esto es una gran cantidad de trabajo que, en última instancia, abarca el terreno que ya se ocupa HTML5, pero, a veces, la compatibilidad nativa no es lo suficientemente buena y es hora de reinventar la rueda.
Cómo trabajar con imágenes y sprites
Un motor no solo sirve para ejecutar el garabato, sino también para trabajar en él. Compartí algunos parámetros de depuración anteriormente. Puedes encontrar el resto en engine.readDebugParams
.
La creación de sprites es una técnica conocida que también usamos para los garabatos. Nos permite ahorrar bytes y disminuir los tiempos de carga, además de facilitar la carga previa.
Sin embargo, también dificulta el desarrollo, ya que cada cambio en las imágenes requeriría volver a crear sprites (en gran medida automatizado, pero aún engorroso). Por lo tanto, el motor admite la ejecución en imágenes sin procesar para el desarrollo, así como sprites para la producción a través de engine.useSprites
, que se incluyen con el código fuente.

También admitimos la carga previa de imágenes a medida que avanzamos y detenemos el garabato si las imágenes no se cargaron a tiempo, todo con una barra de progreso falsa. (Falso porque, lamentablemente, ni siquiera HTML5 puede decirnos cuánto de un archivo de imagen ya se cargó).

En algunas escenas, usamos más de un sprite, no tanto para acelerar la carga con conexiones en paralelo, sino simplemente debido a la limitación de 3/5 millones de píxeles para las imágenes en iOS.
¿Dónde encaja HTML5 en todo esto? No hay mucho de eso arriba, pero la herramienta que escribí para crear sprites o recortar era toda una nueva tecnología web: lienzo, blobs, a[download]. Una de las cosas más interesantes de HTML es que, poco a poco, incorpora elementos que antes se debían hacer fuera del navegador. La única parte que necesitábamos hacer allí era optimizar los archivos PNG.
Cómo guardar el estado entre juegos
Los mundos de Lem siempre se sintieron grandes, vivos y realistas. Sus historias, por lo general, comenzaban sin muchas explicaciones, y la primera página empezaba en medio de la acción, por lo que el lector tenía que encontrar su camino.
Cyberiad no fue la excepción, y queríamos replicar ese sentimiento en el doodle. Empezamos por tratar de no explicar demasiado la historia. Otro elemento importante es la aleatoriedad, que consideramos que se ajustaba a la naturaleza mecánica del universo del libro. Tenemos varias funciones de ayuda que se ocupan de la aleatoriedad y que usamos en muchos lugares.
También queríamos aumentar la capacidad de repetición de otras maneras. Para ello, necesitábamos saber cuántas veces se había terminado el garabato antes. Históricamente, la solución tecnológica correcta para eso es una cookie, pero no funciona para la página principal de Google. Cada cookie aumenta la carga útil de cada página y, una vez más, nos preocupa mucho la velocidad y la latencia.
Afortunadamente, HTML5 nos brinda el almacenamiento web, que es trivial de usar y nos permite guardar y recuperar el recuento general de reproducciones y la última escena que reprodujo el usuario, con mucha más elegancia que las cookies.
¿Qué hacemos con esta información?
- Mostramos un botón de avance rápido que permite pasar las escenas de corte que el usuario ya vio
- mostramos diferentes elementos N durante el final
- aumentamos ligeramente la dificultad del nivel de disparo
- En la tercera y las siguientes reproducciones, te mostramos un pequeño dragón de probabilidad de huevo de pascua de una historia diferente.
Hay varios parámetros de depuración que controlan esto:
?doodle-debug&doodle-first-run
: Haz como si fuera una primera ejecución.?doodle-debug&doodle-second-run
: Imita una segunda ejecución.?doodle-debug&doodle-old-run
: Imita una actividad anterior.
Dispositivos táctiles
Queríamos que el garabato se sintiera como en casa en los dispositivos táctiles. Los más modernos son lo suficientemente potentes como para que el garabato se ejecute muy bien, y experimentar el juego con toques es mucho más divertido que con clics.
Se debían realizar algunos cambios iniciales en la experiencia del usuario. Originalmente, el puntero del mouse era el único lugar que comunicaba que se estaba produciendo una escena de corte o una parte no interactiva. Más adelante, agregamos un pequeño indicador en la esquina inferior derecha, de modo que no tuvimos que depender solo del puntero del mouse (dado que no existen en dispositivos táctiles).
Normal | Ocupado | Se puede hacer clic | Acción al hacer clic | |
---|---|---|---|---|
Trabajo en curso | ![]() |
![]() |
![]() |
![]() |
Final | ![]() |
![]() |
![]() |
![]() |
La mayoría de los elementos funcionaron de inmediato. Sin embargo, las pruebas de usabilidad rápidas e improvisadas de nuestra experiencia táctil mostraron dos problemas: algunos de los objetivos eran demasiado difíciles de presionar y se ignoraban los toques rápidos, ya que solo anulamos los eventos de clic del mouse.
Tener elementos del DOM transparentes en los que se puede hacer clic por separado ayudó mucho, ya que pude cambiar su tamaño independientemente de las imágenes. Introduje un padding adicional de 15 píxeles para 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 los entornos de mouse, solo para complacer al Sr. Fitts).
En cuanto al otro problema, me aseguré de adjuntar y probar los controladores de inicio y fin de toque adecuados, 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 (destacar con un toque, texto destacado con un toque).
¿Cómo detectamos si un dispositivo determinado que ejecuta el garabato admite la función de toque? De forma lenta. En lugar de averiguarlo de forma anticipada, solo usamos nuestros IQ combinados para deducir que el dispositivo admite la función táctil… después de recibir el primer evento de inicio táctil.
Cómo personalizar el puntero del mouse
Pero no todo se basa en la función táctil. Uno de nuestros principios rectores fue incluir la mayor cantidad posible de elementos en el universo del garabato. La IU de la barra lateral pequeña (adelante rápido, signo de interrogación), la información sobre herramientas y, sí, el puntero del mouse.
¿Cómo puedo personalizar un puntero del mouse? Algunos navegadores permiten cambiar el cursor del mouse vinculando a un archivo de imagen personalizado. Sin embargo, esto no es muy compatible y también es un poco restrictivo.
Si no es así, ¿qué es? Bueno, ¿por qué no hacer que el puntero del mouse sea solo otro elemento del garabato? Esto funciona, pero tiene algunas salvedades, principalmente:
- debes poder quitar el puntero del mouse nativo
- Debes ser bastante 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 algunas técnicas: usar un archivo .cur
vacío como resguardo, especificar un comportamiento concreto para algunos navegadores y, hasta, codificar otros de forma fija fuera de la experiencia.
El otro es relativamente trivial a primera vista, pero, como el puntero del mouse es solo otra parte del universo del garabato, también heredará todos sus problemas. El mayor desafío es Si la velocidad de fotogramas del garabato es baja, la velocidad de fotogramas del puntero del mouse también lo será, lo que tiene consecuencias graves, ya que el puntero del mouse, como extensión natural de la mano, debe ser responsivo sin importar nada. (Las personas que usaron Commodore Amiga en el pasado ahora asienten con vigor).
Una solución un tanto compleja a ese problema es desacoplar el puntero del mouse del bucle de actualización normal. Hicimos exactamente eso, en un universo alternativo en el que no necesito dormir. ¿Hay una solución más simple para este caso? Solo se vuelve al puntero del mouse nativo si la velocidad de fotogramas continua cae por debajo de 20 fps. (Aquí es donde resulta útil la frecuencia de fotogramas continua). Si reaccionáramos a la velocidad de fotogramas actual y esta oscilara alrededor de 20 fps, el usuario vería que el puntero del mouse personalizado se oculta y se muestra todo el tiempo). Esto nos lleva a lo siguiente:
Rango de velocidad de fotogramas | Comportamiento |
---|---|
>10 fps | Disminuye 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 del personalizado. |
De 20 a 60 fps | Operación normal. |
>60 fps | Limita la velocidad para que la velocidad de fotogramas no supere este valor. |
Ah, 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 los universos ficticios.
Conclusión
No es un motor perfecto, pero no intenta serlo. Se desarrolló junto con el garabato de Lem y es muy específico para él. Eso está bien. “La optimización prematura es la raíz de todos los problemas”, como dijo Don Knuth, y no creo que tenga sentido escribir un motor en aislamiento primero y solo aplicarlo más adelante. La práctica informa a la teoría tanto como la teoría informa a la práctica. En mi caso, se descartó el código, se volvieron a escribir varias partes una y otra vez, y se notaron muchos elementos comunes después de la publicación, en lugar de antes. Pero, al final, lo que tenemos aquí 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 posible.
Espero que lo anterior aclare algunas de las opciones de diseño y las compensaciones que tuvimos que tomar, y cómo usamos HTML5 en una situación específica de la vida real. Ahora, juega con el código fuente, pruébalo y cuéntanos qué opinas.
Yo mismo lo hice. La siguiente imagen estuvo en vivo en los últimos días, contando regresivamente hasta las primeras horas del 23 de noviembre de 2011 en Rusia, que fue la primera zona horaria que vio el Doodle de Lem. Quizás sea una tontería, pero, al igual que los garabatos, las cosas que parecen insignificantes a veces tienen un significado más profundo. Este contador fue una buena “prueba de esfuerzo” para el motor.

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