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 con WebGL para dispositivos móviles. En este artículo, analizaremos los desafíos, los problemas y las soluciones que encontramos al crear el resto de la interfaz HTML5.

Tres versiones del mismo sitio

Comencemos por hablar un poco sobre cómo adaptar este experimento para que funcione tanto en computadoras de escritorio como en dispositivos móviles desde una perspectiva del tamaño de la pantalla y la capacidad del dispositivo.

Todo el proyecto se basa en un estilo muy "cinemático", en el que, desde el punto de vista de nuestro diseño, queríamos mantener la experiencia dentro de un marco fijo orientado al paisaje para que no produjera la magia de la película. Dado que una gran parte del proyecto consiste en mini “juegos interactivos”, tampoco tendría sentido dejar que desbordaran el marco.

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

Las águilas nos acaban de dejar en la página de destino.
Las águilas nos acaban de dejar en la página de destino.

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

Estamos usando datos de usuario-agente para detectar dispositivos móviles y una prueba de tamaño del viewport para segmentar las tablets entre esos dispositivos (de 645 px o más). Cada modo diferente puede representar todas las resoluciones, ya que el diseño se basa en consultas de medios o en un posicionamiento relativo o porcentual con JavaScript.

Dado que los diseños en este caso no se basan en cuadrículas o reglas y son bastante únicos entre las diferentes secciones, en realidad depende del elemento y del escenario específicos en cuanto a qué puntos de interrupción o estilos usar. Sucedió más de una vez que habíamos configurado el diseño perfecto con buenos mezclas 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 de encabezado para poder 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 360 x 320 aproximadamente, lo cual ha sido un gran desafío a la hora de crear una experiencia web inmersiva. En las 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 tanto el modo horizontal como el modo vertical hasta las experiencias interactivas, donde les pedimos que cambien la orientación del dispositivo a la posición horizontal. El argumento en contra de esto era que no es tan envolvente en el modo vertical como en el horizontal, pero el tamaño del sitio era bastante bueno, así que conservamos el diseño.

Es importante tener en cuenta que el diseño no debe mezclarse con la detección de atributos, como el tipo de entrada, la orientación del dispositivo, sensores, etc. Esos atributos pueden existir en todos estos modos y deben abarcar todos. Un ejemplo de ello es admitir el mouse y la pantalla táctil al mismo tiempo. La compensación de la Retina por la calidad es lo más importante, pero más que nada el rendimiento, ya que, a veces, una calidad menor es mejor. Por ejemplo, el lienzo es la mitad de la resolución en las experiencias de WebGL en pantallas Retina, que, de lo contrario, tendrían que renderizarse cuatro veces la cantidad de píxeles.

Durante el desarrollo, usamos con frecuencia la herramienta de emulador en Herramientas para desarrolladores, especialmente en Chrome Canary, que tiene nuevas funciones mejoradas y muchos ajustes predeterminados. Es una buena manera de validar rápidamente el diseño. Aun así, necesitábamos realizar pruebas en dispositivos reales con regularidad. Uno de ellos fue que el sitio se está adaptando a la pantalla completa. Las páginas con desplazamiento vertical ocultan la IU del navegador cuando se desplazan en la mayoría de los casos (por el momento, Safari en iOS7 tiene problemas con esto), pero tuvimos que ajustar todo, independientemente de eso. También usamos un ajuste predeterminado en el emulador y cambiamos la configuración del tamaño de la pantalla para simular la pérdida de espacio disponible. Realizar pruebas en dispositivos reales también es importante para supervisar el consumo y el rendimiento de la memoria.

Cómo controlar el estado

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

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

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

Exhibición de las ubicaciones

Para mostrar los hermosos parámetros de configuración y los personajes de la Tierra Media, construimos un sistema modular de componentes de imagen y texto que puedes arrastrar o deslizar de forma horizontal. No habilitamos una barra de desplazamiento en este caso porque queremos tener diferentes velocidades en distintos rangos, como en las secuencias de imágenes en las que detienes el movimiento hacia los lados hasta que el clip se haya reproducido.

El salón de Thranduil
Cronograma de Thranduil's Hall

La línea de tiempo

Cuando comenzó el desarrollo, no sabíamos el contenido de los módulos para cada ubicación. Lo que sabíamos es que queríamos mostrar los distintos tipos de información y medios de comunicación mediante plantillas en un cronograma horizontal que nos diera la libertad de realizar seis presentaciones de ubicaciones distintas sin tener que reconstruir todo seis veces. Para administrar esto, creamos un controlador de cronograma que se encarga del desplazamiento lateral de sus módulos en función de la configuración y los comportamientos de estos.

Componentes de comportamiento y módulos

Los diferentes módulos a los que agregamos compatibilidad son secuencia de imágenes, imagen estática, escena de paralaje, escena de cambio de enfoque y texto.

El módulo de escena de paralaje tiene un fondo opaco con una cantidad personalizada de capas que escuchan el progreso del viewport para conocer las posiciones exactas.

La escena de foco de cambio es una variante del bucket de paralaje, con la adición de que usamos dos imágenes para cada capa con fundido de entrada y salida para simular un cambio de enfoque. Intentamos usar el filtro de desenfoque, pero sigue siendo muy costoso, por lo que esperaremos a los sombreadores de CSS para ello.

El contenido del módulo de texto se puede arrastrar con el complemento Draggable de TweenMax. También puedes usar la rueda del mouse o deslizar con dos dedos para desplazarte verticalmente. Ten en cuenta 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 y parámetros de configuración de objetivos. Traduce para mover un elemento, ajustar la escala al zoom, hotspots para superposición de información, depurar métricas para realizar pruebas visuales, una superposición de título de inicio, una capa de destello y mucho más. Estos se agregarán al DOM o controlarán su elemento objetivo dentro del módulo.

De esta manera, podemos crear las diferentes ubicaciones con solo un archivo de configuración que defina qué recursos cargar y configurar los diferentes tipos de módulos y componentes.

Secuencias de imágenes

El más desafiante de los módulos con respecto al rendimiento y al tamaño de descarga es la secuencia de imágenes. Hay mucho para leer sobre este tema. En dispositivos móviles y tabletas, la reemplazamos por una imagen fija. Son demasiados datos para decodificarlos y almacenarlos en la memoria si queremos una calidad decente en los dispositivos móviles. Probamos varias soluciones alternativas: usamos una imagen de fondo y una hoja de objetos en primer lugar, pero eso provocó problemas de memoria y se atrajo cuando la GPU necesitaba intercambiar hojas de objetos. Luego intentamos intercambiar los elementos img, pero también era demasiado lento. Dibujar un marco desde una hoja de objetos en un lienzo fue lo más eficaz, así que empezamos a optimizarlo. Para ahorrar tiempo de procesamiento en cada fotograma, los datos de imágenes que se escribirán en el lienzo se procesan previamente con un lienzo temporal y se guardan con putImageData() en un array, decodificados y listos para usar. La hoja de objetos original puede recolectarse como basura y almacenar solo la cantidad mínima de datos necesarios en la memoria. Quizás, en realidad, sea menos almacenar imágenes no decodificadas, pero obtenemos un mejor rendimiento mientras arrastramos la secuencia de esta manera. Los marcos son bastante pequeños, de solo 640 x 400, pero solo se podrán ver durante el arrastre. Cuando te detienes, se carga una imagen de alta resolución y se atenúa 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 objeto se generan con Imagemagick. Aquí tienes un ejemplo simple en GitHub que muestra cómo crear una hoja de objeto con todas las imágenes dentro de una carpeta.

Cómo animar los módulos

Para colocar los módulos en la línea de tiempo, una representación oculta de esta, que se muestra fuera de la pantalla, registra el "cabezal de reproducción" y el ancho del cronograma. Esto se puede hacer solo con código, pero fue bueno con una representación visual durante el desarrollo y la depuración. Cuando se ejecuta de forma real, solo se actualiza cuando se cambia el tamaño para establecer las dimensiones. Algunos módulos ocupan el viewport y otros tienen su propia proporción, por lo que era un poco complicado escalar y posicionar todo en todas las resoluciones, de modo que todo fuera visible y no demasiado recortado. 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 en sí. Cuando se hace un movimiento de paralaje, suele ser difícil calcular la posición inicial y final de los objetos para sincronizarlos con la posición esperada cuando se ven. Es bueno saber exactamente cuándo un módulo entra en la vista, reproduce su cronograma interno y cuándo vuelve a animarse fuera de la vista.

Cada módulo tiene una sutil capa negra en la parte superior que ajusta su opacidad para que sea totalmente transparente cuando se encuentra 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 funcional a una versión de lanzamiento sin bloqueos implica pasar de adivinar a saber qué sucede en el navegador. Aquí es donde las Herramientas para desarrolladores de Chrome son tus mejores aliados.

Hemos dedicado mucho tiempo a optimizar el sitio. Forzar la aceleración de hardware es una de las herramientas más importantes, por supuesto, para obtener animaciones fluidas. Pero también puede cazar columnas coloridas y rectángulos rojos en las Herramientas para desarrolladores de Chrome. Hay muchos artículos interesantes sobre estos temas y deberías leerlos todos. La recompensa por quitar fotogramas que se omiten es instantánea, pero también lo es la frustración cuando regresan. Y lo harán. Es un proceso continuo que necesita iteraciones.

Me gusta usar TweenMax de Greensock para la interpolación de propiedades, transformaciones y CSS. Piensa en contenedores y visualiza tu estructura a medida que agregas capas nuevas. 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 quieres interpolar valores 2D. Para mantener la capa en 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 2D. Es fácil olvidarse cuando combinas interpolaciones de CSS y JavaScript para establecer estilos.

No fuerces la aceleración de hardware cuando no sea necesaria. La memoria de la GPU puede llenarse rápidamente y generar resultados no deseados cuando deseas acelerar muchos contenedores de hardware, en especial en iOS, donde la memoria tiene más restricciones. Para cargar recursos más pequeños, escalarlos con CSS e inhabilitar algunos de los efectos en el modo móvil, se realizaron enormes mejoras.

Fugas de memoria era otro campo en el que necesitábamos mejorar nuestras habilidades. Cuando navegas por las diferentes interfaces de WebGL, se crean muchos objetos, materiales, texturas y geometría. Si esos elementos no están listos para la recolección de elementos no utilizados cuando sales y quitas la sección, 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 eliminación con errores.
Salir de una sección con una función de eliminación con errores
¡Mucho mejor!
Mucho mejor

Para encontrar la fuga, en Herramientas para desarrolladores, fue bastante sencillo usar el flujo de trabajo, registrar la línea de tiempo y capturar instantáneas del montón. Es más fácil si hay objetos específicos que puedas filtrar, como geometría 3D o una biblioteca específica. En el ejemplo anterior, resultó que la escena en 3D todavía estaba disponible y que no se borró un array que almacenaba geometría. Si te resulta difícil localizar dónde vive el objeto, hay una característica interesante que te permite verlo, llamada rutas de retención. Simplemente haz clic en el objeto que desees inspeccionar en la instantánea del montón y obtendrás la información en el panel que aparece a continuación. Usar una buena estructura con objetos más pequeños ayuda a ubicar las referencias.

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

En general, es saludable pensar dos veces antes de manipular el DOM. Cuando lo hagas, piensa en la eficiencia. No manipules el DOM dentro de un bucle de juego si puedes ayudarlo. 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 principal más cercano.

Retrasa las dimensiones de lectura de los elementos recién agregados o cuando quites o agregues clases si experimentas errores de diseño. También puedes asegurarte de que se active el diseño. A veces, el lote del navegador cambia a los estilos y no se actualiza después de la siguiente activación de diseño. Esto puede ser un gran problema a veces, pero está ahí por una razón, así que trata de aprender cómo funciona detrás de escena y ganarás mucho.

Pantalla completa

Cuando está disponible, tienes la opción de poner el sitio en modo de pantalla completa en el menú a través de la API de pantalla completa. Pero en los dispositivos también está la decisión del navegador de ponerlo en pantalla completa. Anteriormente, Safari en iOS tenía un truco 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 esperemos recibir actualizaciones sobre este tema en las próximas actualizaciones, ya que rompió muchas apps web.

Recursos

Instrucciones animadas para los experimentos.
Instrucciones animadas para los experimentos.

En todo el sitio tenemos muchos tipos de recursos diferentes, usamos imágenes (PNG y JPEG), SVG (intercalados y de fondo), hojas de objeto (PNG), fuentes de íconos personalizados y animaciones de Adobe Edge. Usamos archivos PNG para recursos y animaciones (hojas de objeto) donde el elemento no puede basarse en vectores; de lo contrario, tratamos de usar SVG tanto como sea posible.

El formato vectorial significa que no hay pérdida de calidad, incluso si lo escalamos. 1 archivo para todos los dispositivos.

  • Tamaño de archivo pequeño.
  • Podemos animar cada parte por separado (ideal para animaciones avanzadas). Por ejemplo, ocultamos el "subtítulo" del logotipo del Hobbit (la desolación de Smaug) cuando se reduce su tamaño.
  • Se puede incorporar como una etiqueta SVG HTML o se puede usar como 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 escalabilidad y se usan en lugar de SVG para elementos pequeños, como los íconos en los que solo necesitamos cambiar el color (desplazamiento, activo, etc.). Los íconos también son muy fáciles de reutilizar: solo debes configurar la propiedad de “contenido” de CSS de un elemento.

Animaciones

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

Todavía siento que tenemos un largo camino por recorrer antes de tener un flujo de trabajo perfecto para administrar recursos y animaciones hechas a mano en la Web. Esperamos ver cómo evolucionarán herramientas como Edge. Siéntete libre de agregar sugerencias sobre otros flujos de trabajo y herramientas de animación en los comentarios.

Conclusión

Ahora, cuando se lancen todas las partes del proyecto y veamos el resultado final, debo decir que estamos bastante impresionados con el estado de los navegadores móviles modernos. Cuando comenzamos este proyecto, teníamos expectativas mucho menores sobre el nivel de fluidez, integración y rendimiento que podríamos tener. Fue una gran experiencia de aprendizaje para nosotros, y todo el tiempo dedicado a las iteraciones y pruebas (muchas veces) ha mejorado nuestra comprensión de cómo funcionan los navegadores modernos. Y eso es lo que se necesita si queremos acortar el tiempo de producción de este tipo de proyectos, pasar de las conjeturas al conocimiento.