El front-end de la Tierra Media

Explicación del desarrollo multidispositivo

En nuestro primer artículo sobre el desarrollo del experimento de Chrome Un viaje por la Tierra Media, nos enfocamos en el desarrollo de WebGL para dispositivos móviles. En este artículo, analizamos los desafíos, los problemas y las soluciones que encontramos cuando creamos el resto del frontend de HTML5.

Tres versiones del mismo sitio

Comencemos hablando un poco sobre la adaptación de este experimento para que funcione en computadoras de escritorio y dispositivos móviles desde una perspectiva de tamaño de pantalla y capacidades del dispositivo.

Todo el proyecto se basa en un estilo muy “cinematográfico”, en el que, en términos de diseño, queríamos mantener la experiencia dentro de un marco fijo orientado horizontalmente para conservar la magia de la película. Dado que una gran parte del proyecto consiste en mini “juegos” interactivos, tampoco tendría sentido permitir que se salgan del marco.

Podemos tomar la página de destino como ejemplo de cómo adaptamos el diseño para diferentes tamaños.

Las águilas nos dejaron en la página de destino.
Los águilas nos dejaron en la página de destino.

El sitio tiene tres modos diferentes: computadora de escritorio, tablet y dispositivo móvil. No solo para controlar el diseño, sino porque necesitamos controlar los recursos cargados en el entorno de ejecución y agregar varias optimizaciones de rendimiento. Con dispositivos que tienen una resolución más alta que las computadoras de escritorio y las laptops, pero que tienen un rendimiento peor que los teléfonos, no es una tarea fácil definir el conjunto definitivo de reglas.

Usamos datos del usuario-agente para detectar dispositivos móviles y una prueba de tamaño de viewport para segmentar anuncios para tablets entre ellos (645 px y superiores). De hecho, cada modo diferente puede renderizar todas las resoluciones, ya que el diseño se basa en consultas de medios o en el posicionamiento relativo o porcentual con JavaScript.

Dado que los diseños en este caso no se basan en cuadrículas ni reglas y son bastante únicos entre las diferentes secciones, realmente depende del elemento y la situación específicos qué puntos de inflexión o estilos usar. Más de una vez, configuramos el diseño perfecto con buenas combinaciones de Sass y consultas de medios, y luego tuvimos que agregar un efecto basado en la posición del mouse o en objetos dinámicos, y terminamos reescribiendo todo en JavaScript.

También agregamos una clase con el modo actual en la etiqueta head para que podamos usar esa información en nuestros estilos, como en este ejemplo (en SCSS):

.loc-hobbit-logo {

  // Default values here.

  .desktop & {
     // Applies only in desktop mode.
  }

 .tablet &, .mobile & {
   
   // Different asset for mobile and tablets perhaps.

   @media screen and (max-height: 760px), (max-width: 760px) {
     // Breakpoint-specific styles.
   }

   @media screen and (max-height: 570px), (max-width: 400px) {
     // Breakpoint-specific styles.
   }
 }
}

Admitimos todos los tamaños hasta aproximadamente 360 x 320, lo que ha sido un desafío cuando se trata de crear una experiencia web envolvente. En computadoras de escritorio, tenemos un tamaño mínimo antes de mostrar las barras de desplazamiento porque queremos que experimentes el sitio en un viewport más grande si es posible. En los dispositivos móviles, decidimos permitir el modo horizontal y vertical hasta las experiencias interactivas, en las que te pedimos que gires el dispositivo a la posición horizontal. El argumento en contra era que no es tan envolvente en orientación vertical como en horizontal, pero el sitio se adaptó bastante bien, así que lo conservamos.

Es importante tener en cuenta que el diseño no debe confundirse con la detección de componentes, como el tipo de entrada, la orientación del dispositivo, los sensores, etcétera. Esas funciones pueden existir en todos estos modos y deben abarcar todos. Un ejemplo es admitir el mouse y la función táctil al mismo tiempo. La compensación de retina para la calidad, pero sobre todo para el rendimiento, es otra opción. A veces, una calidad menor es mejor. A modo de ejemplo, el lienzo tiene la mitad de la resolución en las experiencias de WebGL en pantallas Retina, que de otro modo tendrían que renderizar cuatro veces la cantidad de píxeles.

Usamos con frecuencia la herramienta de emulador en DevTools durante el desarrollo, en especial en Chrome Canary, que tiene nuevas funciones mejoradas y muchos parámetros preestablecidos. Es una buena forma de validar rápidamente el diseño. Aún necesitábamos realizar pruebas en dispositivos reales con frecuencia. Uno de los motivos es que el sitio se adapta a pantalla completa. Las páginas con desplazamiento vertical ocultan la IU del navegador cuando se desplazan en la mayoría de los casos (Safari en iOS 7 tiene problemas con esto actualmente), pero tuvimos que ajustar todo independientemente de eso. También usamos un parámetro de configuración predeterminado en el emulador y cambiamos el parámetro de configuración del tamaño de la pantalla para simular la pérdida de espacio disponible. Las pruebas en dispositivos reales también son importantes para supervisar el consumo de memoria y el rendimiento.

Controla el estado

Después de la página de destino, llegamos al mapa de la Tierra Media. ¿Notaste que cambió la URL? El sitio es una aplicación de una sola página que usa la History API para controlar el enrutamiento.

Cada sección del sitio es su propio objeto que hereda un modelo de texto de funcionalidad, como elementos DOM, transiciones, carga de recursos, eliminación, etc. Cuando exploras diferentes partes del sitio, se inician las secciones, se agregan y quitan elementos del DOM, y se cargan los recursos de la sección actual.

Dado que el usuario puede presionar el botón Atrás del navegador o navegar por el menú en cualquier momento, todo lo que se crea debe eliminarse en algún momento. Los tiempos de espera y las animaciones deben detenerse y descartarse, de lo contrario, provocarán comportamientos no deseados, errores y fugas de memoria. Esta no siempre es una tarea fácil, en especial cuando se acercan las fechas límite y necesitas ingresar todo lo más rápido posible.

Cómo mostrar las ubicaciones

Para mostrar los hermosos escenarios y los personajes de la Tierra Media, creamos un sistema modular de componentes de imagen y texto que puedes arrastrar o deslizar horizontalmente. No habilitamos una barra de desplazamiento aquí, ya que queremos tener diferentes velocidades en diferentes rangos, como en las secuencias de imágenes en las que detienes el movimiento lateral hasta que se reproduce el clip.

Salón de Thranduil
Cronología de la sala de Thranduil

La línea de tiempo

Cuando comenzó el desarrollo, no conocíamos el contenido de los módulos de cada ubicación. Sabíamos que queríamos una forma basada en plantillas de mostrar diferentes tipos de información y contenido multimedia en un cronograma horizontal que nos diera la libertad de tener seis presentaciones de ubicaciones diferentes sin tener que volver a crear todo seis veces. Para administrar esto, creamos un controlador de línea de tiempo que controla el desplazamiento de sus módulos en función de la configuración y los comportamientos de los módulos.

Módulos y componentes de comportamiento

Los diferentes módulos para los que agregamos compatibilidad son la secuencia de imágenes, la imagen fija, la escena de paralaje, la escena de cambio de enfoque y el texto.

El módulo de escena de paralaje tiene un fondo opaco con una cantidad personalizada de capas que escuchan el progreso de la vista del puerto para obtener posiciones exactas.

La escena de cambio de enfoque es una variante del bucket de paralaje, con la diferencia de que usamos dos imágenes para cada capa que se atenúan y se desvanecen para simular un cambio de enfoque. Intentamos usar el filtro de desenfoque, pero aún es demasiado costoso, por lo que esperaremos a los sombreadores de CSS para esto.

El contenido del módulo de texto se puede arrastrar con el complemento TweenMax Draggable. También puedes usar la rueda del mouse o el deslizamiento con dos dedos para desplazarte verticalmente. Observa el throw-props-plugin que agrega la física de estilo de deslizamiento cuando deslizas y sueltas.

Los módulos también pueden tener diferentes comportamientos que se agregan como un conjunto de componentes. Todos tienen sus propios selectores de segmentación y parámetros de configuración. Traduce para mover un elemento, escalar para acercar, hotspots para superposición de información, métricas de depuración para pruebas visuales, una superposición de título de inicio, una capa de destello y mucho más. Se agregarán al DOM o controlarán su elemento de destino dentro del módulo.

Con esto, podemos crear las diferentes ubicaciones con solo un archivo de configuración que define qué recursos cargar y configurar los diferentes tipos de módulos y componentes.

Secuencias de imágenes

El módulo más desafiante desde el punto de vista del rendimiento y el tamaño de descarga es la secuencia de imágenes. Hay mucho material de lectura sobre este tema. En dispositivos móviles y tablets, lo reemplazamos por una imagen inmóvil. Son demasiados datos para decodificar y almacenar en la memoria si queremos una calidad decente en dispositivos móviles. Probamos varias soluciones alternativas, primero con una imagen de fondo y una hoja de sprites, pero esto generó problemas de memoria y retrasos cuando la GPU necesitaba cambiar entre hojas de sprites. Luego, intentamos intercambiar los elementos img, pero también era demasiado lento. Dibujar un fotograma de una hoja de sprites en un lienzo era la opción con mejor rendimiento, así que comenzamos a optimizarla. Para ahorrar tiempo de procesamiento en cada fotograma, los datos de imagen que se escriben en el lienzo se procesan previamente a través de un lienzo temporal y se guardan con putImageData() en un array, se decodifican y están listos para usarse. Luego, se puede realizar la recolección de elementos no utilizados en la hoja de sprites original, y solo almacenamos la cantidad mínima de datos necesarios en la memoria. Tal vez sea menos almacenar imágenes sin decodificar, pero obtenemos un mejor rendimiento cuando limpiamos la secuencia de esta manera. Los fotogramas son bastante pequeños, solo 640 × 400, pero solo se verán durante el arrastre. Cuando te detengas, se cargará una imagen de alta resolución y se atenuará rápidamente.

var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;

var ctx = canvas.getContext('2d');
ctx.drawImage(sheet, 0, 0);

var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;

var canvasPaste = canvas.cloneNode(false);
canvasPaste.width = tileWidth;
canvasPaste.height = tileHeight;

var i, j, canvasPasteTemp, imgData, 
var currentIndex = 0;
var startIndex = index * 16;
for (i = 0; i < tilesY; i++) {
  for (j = 0; j < tilesX; j++) {
    // Store the image data of each tile in the array.
    canvasPasteTemp = canvasPaste.cloneNode(false);
    imgData = ctx.getImageData(j * tileWidth, i * tileHeight, tileWidth, tileHeight);
    canvasPasteTemp.getContext('2d').putImageData(imgData, 0, 0);

    list[ startIndex + currentIndex ] = imgData;

    currentIndex++;
  }
}

Las hojas de sprites se generan con Imagemagick. Este es un ejemplo simple en GitHub que muestra cómo crear una hoja de sprites de todas las imágenes dentro de una carpeta.

Animación de los módulos

Para colocar los módulos en la línea de tiempo, una representación oculta de la línea de tiempo, que se muestra fuera de la pantalla, realiza un seguimiento del "punto de reproducción" y el ancho de la línea de tiempo. Esto se puede hacer solo con código, pero es mejor con una representación visual durante el desarrollo y la depuración. Cuando se ejecuta de verdad, solo se actualiza cuando se cambia el tamaño para establecer las dimensiones. Algunos módulos ocupan toda la vista del cliente y otros tienen su propia relación, por lo que fue un poco complicado escalar y posicionar todo en todas las resoluciones para que todo sea visible y no se recorte demasiado. Cada módulo tiene dos indicadores de progreso, uno para la posición visible en la pantalla y otro para la duración del módulo. Cuando se realiza un movimiento de paralaje, a menudo es difícil calcular la posición inicial y final de los objetos para sincronizarlos con la posición esperada cuando están a la vista. Es bueno saber exactamente cuándo un módulo entra en la vista, reproduce su línea de tiempo interna y cuándo vuelve a salir de la vista con animación.

Cada módulo tiene una capa negra sutil en la parte superior que ajusta su opacidad para que sea completamente transparente cuando está en la posición central. Esto te ayuda a enfocarte en un módulo a la vez, lo que mejora la experiencia.

Rendimiento de la página

Pasar de un prototipo en funcionamiento a una versión de lanzamiento sin bloqueos significa pasar de adivinar a saber qué sucede en el navegador. Aquí es donde Chrome DevTools es tu mejor amigo.

Pasamos mucho tiempo optimizando el sitio. Forzar la aceleración de hardware es una de las herramientas más importantes para obtener animaciones fluidas. Pero también busca columnas coloridas y rectángulos rojos en Chrome DevTools. Hay muchos artículos buenos sobre los temas, y debes leerlos todos. La recompensa por quitar los fotogramas salteados es instantánea, pero también lo es la frustración cuando vuelven a aparecer. Y lo harán. Es un proceso continuo que necesita iteraciones.

Me gusta usar TweenMax de Greensock para animar propiedades, transformaciones y CSS. Piensa en contenedores y visualiza tu estructura a medida que agregas nuevas capas. Ten en cuenta que las transformaciones existentes se pueden reemplazar por transformaciones nuevas. El translateZ(0) que forzó la aceleración de hardware en tu clase CSS se reemplaza por una matriz 2D si solo aplicas interpolación a valores 2D. Para mantener la capa en el modo de aceleración en esos casos, usa la propiedad "force3D:true" en la interpolación para crear una matriz 3D en lugar de una matriz 2D. Es fácil olvidarse cuando combinas transiciones de CSS y JavaScript para establecer estilos.

No fuerces la aceleración de hardware cuando no sea necesario. La memoria de la GPU puede llenarse rápidamente y causar resultados no deseados cuando deseas acelerar con hardware muchos contenedores, en especial en iOS, donde la memoria tiene más restricciones. Se realizaron grandes mejoras para cargar recursos más pequeños, escalarlos con CSS y, además, inhabilitar algunos de los efectos en el modo para dispositivos móviles.

Las fugas de memoria fueron otro campo en el que necesitábamos mejorar nuestras habilidades. Cuando se navega entre las diferentes experiencias de WebGL, se crean muchos objetos, materiales, texturas y geometrías. Si no están listos para el recolector de elementos no utilizados cuando salgas de la sección y la quites, es probable que el dispositivo falle después de un tiempo cuando se quede sin memoria.

Salir de una sección con una función de descarte que falla
Salir de una sección con una función de descarte que falla
Mucho mejor.
¡Mucho mejor!

Para encontrar la fuga, el flujo de trabajo en DevTools fue bastante sencillo: se grabó el cronograma y se capturaron instantáneas de montón. Es más fácil si hay objetos específicos, como una geometría 3D o una biblioteca específica, que puedas filtrar. En el ejemplo anterior, resultó que la escena 3D aún estaba presente y que no se borró un array que almacenaba la geometría. Si te cuesta ubicar dónde se encuentra el objeto, hay una función útil que te permite ver esto, llamada rutas de retención. Solo haz clic en el objeto que deseas inspeccionar en la instantánea del montón y obtendrás la información en un panel que aparece debajo. Usar una buena estructura con objetos más pequeños ayuda a ubicar tus referencias.

Se hizo referencia a la escena en EffectComposer.
Se hizo referencia a la escena en EffectComposer.

En general, es conveniente pensar dos veces antes de manipular el DOM. Cuando lo hagas, piensa en la eficiencia. Si puedes, no manipules el DOM dentro de un bucle de juego. Almacena referencias en variables para volver a usarlas. Si necesitas buscar un elemento, usa la ruta más corta almacenando referencias a contenedores estratégicos y buscando dentro del elemento ancestro más cercano.

Retrasa la lectura de las dimensiones de los elementos agregados recientemente o cuando quites o agregues clases si experimentas errores de diseño. O bien asegúrate de que se active el diseño. A veces, el lote del navegador cambia a estilos y no se actualizará después del próximo activador de diseño. Esto puede ser un gran problema a veces, pero está ahí por una razón, así que intenta aprender cómo funciona en segundo plano y obtendrás muchos beneficios.

Pantalla completa

Cuando esté disponible, tendrás la opción de poner el sitio en modo de pantalla completa en el menú a través de la API de Fullscreen. Sin embargo, en los dispositivos, también está la decisión del navegador de ponerlo en pantalla completa. Safari para iOS tenía un hack que te permitía controlar eso, pero ya no está disponible, por lo que debes preparar tu diseño para que funcione sin él cuando crees una página sin desplazamiento. Es probable que haya actualizaciones sobre esto en el futuro, ya que afectó a muchas apps web.

Recursos

Instrucciones animadas para los experimentos
Instrucciones animadas para los experimentos.

En todo el sitio, tenemos muchos tipos diferentes de recursos, como imágenes (PNG y JPEG), SVG (incorporados y de fondo), hojas de sprites (PNG), fuentes de íconos personalizadas y animaciones de Adobe Edge. Usamos archivos PNG para los recursos y las animaciones (hojas de sprites) en los que el elemento no puede ser vectorial. De lo contrario, intentamos usar archivos SVG tanto como sea posible.

El formato vectorial no implica pérdida de calidad, incluso si lo ajustamos. 1 archivo para todos los dispositivos

  • Tamaño de archivo pequeño
  • Podemos animar cada parte por separado (ideal para animaciones avanzadas). A modo de ejemplo, ocultamos el “subtítulo” del logotipo de Hobbit (la desolación de Smaug) cuando se reduce.
  • Se puede incorporar como una etiqueta HTML SVG o usarse como una imagen de fondo sin carga adicional (se carga al mismo tiempo que la página HTML).

Los tipos de letra de íconos tienen las mismas ventajas que los SVG en cuanto a la escalabilidad y se usan en lugar de SVG para elementos pequeños, como íconos en los que solo necesitamos poder cambiar el color (colocar el cursor sobre él, activarlo, etc.). Los íconos también son muy fáciles de volver a usar, solo debes establecer la propiedad "content" de CSS de un elemento.

Animaciones

En algunos casos, animar elementos SVG con código puede ser muy lento, en especial cuando la animación debe cambiar mucho durante el proceso de diseño. Para mejorar el flujo de trabajo entre diseñadores y desarrolladores, usamos Adobe Edge para algunas animaciones (las instrucciones antes de los juegos). El flujo de trabajo de animación es muy similar al de Flash, lo que ayudó al equipo, pero hay algunas desventajas, en especial con la integración de las animaciones de Edge en nuestro proceso de carga de recursos, ya que incluye sus propios cargadores y lógica de implementación.

Aún creo que tenemos un largo camino por recorrer antes de tener un flujo de trabajo perfecto para manejar recursos y animaciones hechas a mano en la Web. Esperamos ver cómo evolucionarán herramientas como Edge. No dudes en agregar sugerencias sobre otras herramientas y flujos de trabajo de animación en los comentarios.

Conclusión

Ahora que se lanzaron todas las partes del proyecto y vemos el resultado final, debo decir que estamos bastante impresionados con el estado de los navegadores para dispositivos móviles modernos. Cuando comenzamos este proyecto, teníamos expectativas mucho más bajas sobre lo bien que podríamos integrarlo y lograr que funcionara. Fue una gran experiencia de aprendizaje para nosotros, y todo el tiempo que dedicamos a iterar y probar (mucho) mejoró nuestra comprensión de cómo funcionan los navegadores modernos. Y eso es lo que necesitaremos si queremos acortar el tiempo de producción de este tipo de proyectos, pasando de la suposición al conocimiento.