Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Animation

Optimización de Canvas HTML5: un ejemplo práctico

by
Read Time:34 minsLanguages:

Spanish (Español) translation by Luis Chiabrera (you can also view the original English article)

Si has estado desarrollando JavaScript lo suficiente, es probable que hayas bloqueado tu navegador varias veces. El problema suele ser un error de JavaScript, como un ciclo while infinito; de lo contrario, el próximo sospechoso son las transformaciones o animaciones de páginas, del tipo que implica agregar y eliminar elementos de la página web o animar propiedades de estilo CSS. Este tutorial se centra en la optimización de animaciones producidas con JS y el elemento HTML5  <canvas> .

Este tutorial comienza y termina con lo que el widget de animación HTML5 ve a continuación:

Lo llevaremos con nosotros en un viaje, explorando los diferentes consejos y técnicas de optimización de canvas emergentes y aplicándolos al código fuente de JavaScript del widget. El objetivo es mejorar la velocidad de ejecución del widget y terminar con un widget de animación más fluido y fluido, con JavaScript más ágil y eficiente.

La descarga de la fuente contiene el HTML y JavaScript de cada paso en el tutorial, para que pueda seguirlos desde cualquier punto.

Vamos a dar el primer paso.


Paso 1: reproducir el avance de la película

El widget anterior está basado en el avance de la película de Sintel, una película animada en 3D de la Blender Foundation. Está construido con dos de las adiciones más populares de HTML5: los elementos <canvas> y <video> y .

El <video> carga y reproduce el archivo de video Sintel, mientras que <canvas> genera su propia secuencia de animación tomando instantáneas del video en reproducción y mezclándolo con texto y otros gráficos. Cuando hace clic para reproducir el video, el lienzo cobra vida con un fondo oscuro que es una copia en blanco y negro más grande del video en reproducción. Se copian capturas de pantalla pequeñas y coloreadas del video a la escena, y se deslizan a través de él como parte de una ilustración del rollo de película.

En la esquina superior izquierda, tenemos el título y algunas líneas de texto descriptivo que aparecen y desaparecen a medida que se reproduce la animación. La velocidad de ejecución del guión y las métricas relacionadas se incluyen como parte de la animación, en el pequeño recuadro negro en la esquina inferior izquierda con un gráfico y texto vívido. Veremos este artículo en particular con más detalle más adelante.

Finalmente, hay una gran cuchilla giratoria que sobrevuela la escena al comienzo de la animación, cuyo gráfico se carga desde un archivo de imagen PNG externo.


Paso 2: ver la fuente

El código fuente contiene la mezcla habitual de HTML, CSS y Javascript. El HTML es escaso: solo las etiquetas <canvas> y <video>, encerradas en un contenedor <div>: y , incluidas en un contenedor :

El contenedor <div> recibe una identificación (animationWidget), que actúa como un gancho para todas las reglas de CSS aplicadas a él y sus contenidos (a continuación).

Mientras que HTML y CSS son las especias marinadas y los condimentos, es el JavaScript que es la carne del widget.

  • En la parte superior, tenemos los objetos principales que se usarán a menudo a través del script, incluidas las referencias al elemento canvas y su contexto 2D.
  • La función init () se invoca cada vez que el video comienza a reproducirse y configura todos los objetos utilizados en el script.
  • La función sampleVideo () captura el fotograma actual del video en reproducción, mientras que setBlade () carga una imagen externa requerida por la animación.
  • El ritmo y el contenido de la animación de lienzo están controlados por la función main (), que es como el latido del guión. Ejecutar a intervalos regulares una vez que el video comienza a reproducirse, pinta cada fotograma de la animación borrando primero el lienzo y luego llamando a cada una de las cinco funciones de dibujo del guión:
    • drawBackground()
    • drawFilm()
    • drawTitle()
    • drawDescription()
    • drawStats()

Como sugieren los nombres, cada función de dibujo es responsable de dibujar un elemento en la escena de la animación. La estructuración del código de esta manera mejora la flexibilidad y facilita el mantenimiento en el futuro.

El guion completo se muestra a continuación. Tómese un momento para evaluarlo y ver si puede detectar los cambios que haría para acelerarlo.


Paso 3: Optimización de código: conocer las reglas

La primera regla de optimización del rendimiento del código es: no lo haga

El objetivo de esta regla es desalentar la optimización por el bien de la optimización, ya que el proceso tiene un precio.

Un script altamente optimizado será más fácil de analizar y procesar por el navegador, pero generalmente con una carga para los humanos que les resultará más difícil de seguir y mantener. Siempre que decida que es necesaria alguna optimización, establezca algunos objetivos de antemano para que no se deje llevar por el proceso y se exceda.

El objetivo de optimizar este widget será tener la función main () ejecutándose en menos de 33 milisegundos como se supone que debe, que coincidirá con la velocidad de cuadro de los archivos de video reproducidos (sintel.mp4 y sintel.webm). Estos archivos se codificaron a una velocidad de reproducción de 30 fps (treinta cuadros por segundo), lo que se traduce en aproximadamente 0,33 segundos o 33 milisegundos por cuadro (1 segundo ÷ 30 cuadros).

Como JavaScript dibuja un nuevo marco de animación en el lienzo cada vez que se llama a la función main (), el objetivo de nuestro proceso de optimización será hacer que esta función tome 33 milisegundos o menos cada vez que se ejecute. Esta función se llama repetidamente utilizando un temporizador de JavaScript setTimeout () como se muestra a continuación.

La segunda regla: todavía no.

Esta regla enfatiza el punto de que la optimización siempre se debe hacer al final del proceso de desarrollo cuando ya se ha desarrollado un código completo y funcional. La policía de optimización nos permitirá continuar con esta, ya que la secuencia de comandos del widget es un ejemplo perfecto de un programa completo y listo para el proceso.

La tercera regla: aún no, y el perfil primero.

Esta regla se trata de entender su programa en términos de rendimiento en tiempo de ejecución. La creación de perfiles le ayuda a saber en lugar de adivinar qué funciones o áreas de la secuencia de comandos ocupan más tiempo o si se utilizan con más frecuencia, de modo que puede centrarse en las que están en el proceso de optimización. Es lo suficientemente crítico como para hacer que los principales navegadores se distribuyan con perfiladores de JavaScript incorporados o extensiones que brinden este servicio.

Ejecuté el widget en Profiler en Firebug, y abajo aparece una captura de pantalla de los resultados.


Paso 4: establecer algunas métricas de rendimiento

A medida que ejecutó el widget, estoy seguro de que encontró todas las cosas de Sintel bien, y se sorprendió por el elemento en la esquina inferior derecha del lienzo, el que tiene un gráfico hermoso y texto brillante.

No es solo una cara bonita; esa caja también ofrece algunas estadísticas de rendimiento en tiempo real en el programa en ejecución. Es en realidad un generador de perfiles de Javascript simple y escueto. ¡Está bien! Yo, escuché que te gusta crear perfiles, así que puse un perfilador en tu película, para que puedas perfilarlo mientras miras.

El gráfico rastrea el tiempo de renderizado, calculado midiendo cuánto dura cada ejecución de main () en milisegundos. Como esta es la función que extrae cada cuadro de la animación, es efectivamente la velocidad de cuadros de la animación. Cada línea azul vertical en el gráfico ilustra el tiempo que tarda un cuadro. La línea horizontal roja es la velocidad objetivo, que establecemos en 33 ms para que coincida con las velocidades de cuadro del archivo de video. Justo debajo del gráfico, la velocidad de la última llamada a main () se da en milisegundos.

El generador de perfiles también es una práctica prueba de velocidad de representación del navegador. Por el momento, el tiempo promedio de renderizado en Firefox es de 55 ms, 90 ms en IE 9, 41 ms en Chrome, 148 ms en Opera y 63 ms en Safari. Todos los navegadores se ejecutaban en Windows XP, a excepción de IE 9, que fue perfilado en Windows Vista.

La siguiente métrica debajo de eso es Canvas FPS (marcos de lienzo por segundo), obtenida contando cuántas veces se llama a main () por segundo. El generador de perfiles muestra la última tasa de Canvas FPS cuando el video se está reproduciendo, y cuando termina, muestra la velocidad promedio de todas las llamadas a main ().

La última métrica es el FPS del navegador, que mide el número de veces que el navegador repinta la ventana actual cada segundo. Este solo está disponible si ve el widget en Firefox, ya que depende de una función actualmente disponible solo en ese navegador llamada window.mozPaintCount., Una propiedad de JavaScript que realiza un seguimiento de cuántas veces se repinta la ventana del navegador desde la página web. primero cargado.

Por lo general, los repintados ocurren cuando ocurre un evento o acción que cambia el aspecto de una página, como cuando se desplaza hacia abajo de la página o pasa el mouse sobre un enlace. En realidad, es la velocidad de fotogramas real del navegador, que está determinada por la ocupación de la página web actual.

Para evaluar el efecto que tuvo la animación de lienzo no optimizada en mozPaintCount, eliminé la etiqueta de lienzo y todo el JavaScript para seguir la velocidad de fotogramas del navegador al reproducir solo el video. Mis pruebas se realizaron en la consola de Firebug, utilizando la siguiente función:

Los resultados: la velocidad de fotogramas del navegador era de entre 30 y 32 FPS cuando se estaba reproduciendo el video, y se redujo a 0-1 FPS cuando finalizó el video. Esto significa que Firefox estaba ajustando la frecuencia de repintado de su ventana para que coincida con la del video en reproducción, codificado a 30 fps. Cuando se ejecutó la prueba con la animación de lienzo no optimizada y el video juntos, se desaceleró a 16 fps, ya que el navegador ahora tenía dificultades para ejecutar todo el JavaScript y volver a pintar su ventana a tiempo, realizando tanto la reproducción de video como las animaciones de lienzo, lento.

Ahora comenzaremos a modificar nuestro programa, y mientras lo hacemos, realizaremos un seguimiento de los tiempos de procesamiento, Canvas FPS y FPS del navegador para medir los efectos de nuestros cambios.


Paso 5: use requestAnimationFrame ()

Los dos últimos fragmentos de código JavaScript anteriores utilizan las funciones del temporizador setTimeout () y setInterval (). Para usar estas funciones, especifique un intervalo de tiempo en milisegundos y la función de devolución de llamada que desea ejecutar después de que transcurra el tiempo. La diferencia entre los dos es que setTimeout () llamará a su función una sola vez, mientras que setInterval () la llama repetidamente.

Si bien estas funciones siempre han sido herramientas indispensables en el kit del animador de JavaScript, tienen algunas fallas:

Primero, el intervalo de tiempo establecido no siempre es confiable. Si el programa todavía está en el medio de ejecutar algo más cuando el intervalo transcurra, la función de devolución de llamada se ejecutará más tarde de lo establecido originalmente, una vez que el navegador ya no esté ocupado. En la función main (), establecemos el intervalo en 33 milisegundos, pero como revela el perfilador, la función se llama cada 148 milisegundos en Opera.

En segundo lugar, hay un problema con el repintado del navegador. Si tuviéramos una función de devolución de llamada que generara 20 cuadros de animación por segundo mientras el navegador repintaba su ventana solo 12 veces por segundo, se perderían 8 llamadas a esa función, ya que el usuario nunca podrá ver los resultados.

Finalmente, el navegador no tiene manera de saber que la función a la que se llama está animando elementos en el documento. Esto significa que si esos elementos se desplazan fuera de la vista, o el usuario hace clic en otra pestaña, la devolución de llamada seguirá ejecutándose repetidamente, desperdiciando ciclos de CPU.

El uso de requestAnimationFrame () resuelve la mayoría de estos problemas, y se puede usar en lugar de las funciones del temporizador en animaciones HTML5. En lugar de especificar un intervalo de tiempo, requestAnimationFrame () sincroniza las llamadas a la función con los repintados de la ventana del navegador. Esto da como resultado una animación más fluida y consistente, ya que no se eliminan fotogramas, y el navegador puede realizar más optimizaciones internas sabiendo que hay una animación en progreso.

Para reemplazar setTimeout () con requestAnimationFrame en nuestro widget, primero agregamos la siguiente línea en la parte superior de nuestro script:

Como la especificación aún es bastante nueva, algunos navegadores o versiones de navegador tienen sus propias implementaciones experimentales, esta línea se asegura de que el nombre de la función apunte al método correcto si está disponible, y vuelve a setTimeout () si no es así. Luego, en la función main (), cambiamos esta línea:

...a:

El primer parámetro toma la función de devolución de llamada, que en este caso es la función main (). El segundo parámetro es opcional y especifica el elemento DOM que contiene la animación. Se supone que debe utilizarse para calcular optimizaciones adicionales.

Tenga en cuenta que la función getStats () también utiliza un setTimeout (), pero lo dejamos en su lugar ya que esta función en particular no tiene nada que ver con la animación de la escena. requestAnimationFrame () fue creado específicamente para animaciones, por lo que si su función de devolución de llamada no está haciendo animación, aún puede usar setTimeout () o setInterval ().


Paso 6: utiliza la API de visibilidad de página

En el último paso hicimos que requestAnimationFrame potenciara la animación del lienzo, y ahora tenemos un nuevo problema. Si comenzamos a ejecutar el widget, luego minimizamos la ventana del navegador o cambiamos a una nueva pestaña, la tasa de repinte de la ventana del widget se reduce para ahorrar energía. Esto también ralentiza la animación del lienzo, ya que ahora está sincronizado con la velocidad de repintado, lo que sería perfecto si el video no se reproducía hasta el final.

Necesitamos una forma de detectar cuándo no se está viendo la página para que podamos pausar el video en reproducción; aquí es donde la API de visibilidad de la página viene al rescate.

La API contiene un conjunto de propiedades, funciones y eventos que podemos usar para detectar si una página web está a la vista u oculta. Luego podemos agregar un código que ajusta el comportamiento de nuestro programa en consecuencia. Haremos uso de esta API para detener el video de reproducción del widget siempre que la página esté inactiva.

Comenzamos agregando un nuevo oyente de eventos a nuestro script:

Luego viene la función del controlador de eventos:


Paso 7: Para formas personalizadas, dibuja todo el camino a la vez

Las rutas se utilizan para crear y dibujar formas y contornos personalizados en el elemento <canvas> , que en todo momento tendrá una ruta activa.

Una ruta contiene una lista de subrutas, y cada subruta se compone de puntos de coordenadas de lienzo unidos entre sí por una línea o una curva. Todas las funciones de trazado y dibujo son propiedades del objeto de contexto del lienzo y se pueden clasificar en dos grupos.

Existen las funciones de subpaso, que se usan para definir un subpaso e incluyen lineTo (), quadraticCurveTo (), bezierCurveTo () y arc (). Luego tenemos stroke () y fill (), las funciones de trazado / subpath. El uso de stroke () producirá un contorno, mientras que el fill () genera una forma rellena por un color, un degradado o un patrón.

Al dibujar formas y contornos en el lienzo, es más eficiente crear primero la ruta completa, luego simplemente aplicar un stroke () o fill () una vez, en lugar de definir y dibujar cada fuente a la vez. Tomando el gráfico del perfilador descrito en el Paso 4 como ejemplo, cada línea azul vertical es un subpaso, mientras que todas juntas conforman la ruta actual completa.

El método stroke () se está llamando actualmente dentro de un bucle que define cada subruta:

Este gráfico se puede dibujar mucho más eficiente definiendo primero todos los sub-caminos, y luego dibujando toda la ruta actual a la vez, como se muestra a continuación.


Paso 8: Usa un lienzo sin pantalla para construir la escena

Esta técnica de optimización se relaciona con la del paso anterior, ya que ambas se basan en el mismo principio de minimizar el repintado de páginas web.

Cada vez que ocurre algo que cambia la apariencia o el contenido de un documento, el navegador debe programar una operación de repintado poco después para actualizar la interfaz. Los repintados pueden ser una operación costosa en términos de ciclos de CPU y potencia, especialmente para páginas densas con muchos elementos y animaciones. Si está creando una escena de animación compleja sumando muchos elementos de a uno por vez a <canvas>, cada nueva adición puede desencadenar un repintado completo.

Es mejor y mucho más rápido crear la escena en una pantalla (en memoria) , y una vez hecho, pintar toda la escena una sola vez en la pantalla, visible .

Justo debajo del código que hace referencia al <canvas> del widget y su contexto, agregaremos cinco líneas nuevas que crean un objeto DOM de pantalla externa y unen sus dimensiones con las del <Lienzo> visible original.

Luego haremos como búsqueda y reemplazo en todas las funciones de dibujo para todas las referencias a "mainCanvas" y lo cambiaremos a "osCanvas". Las referencias a "mainContext" se reemplazarán por "osContext". Ahora todo se dibujará en el nuevo lienzo fuera de la pantalla, en lugar del <canvas>  original

Finalmente, agregamos una línea más a main () que pinta lo que está actualmente en el <canvas> fuera de la pantalla en nuestro <canvas> original.


Paso 9: rutas de caché como imágenes de mapa de bits siempre que sea posible

Para muchos tipos de gráficos, usar drawImage () será mucho más rápido que construir la misma imagen en lienzo usando rutas. Si encuentra que una gran porción de su secuencia de comandos se gasta repetidamente dibujando las mismas formas y contornos una y otra vez, puede guardar el navegador al trabajar guardando en caché el gráfico resultante como una imagen de mapa de bits, y luego pintándolo una vez en el lienzo siempre requerido usando drawImage ().

Hay dos formas de hacer esto.

El primero es crear un archivo de imagen externo como una imagen JPG, GIF o PNG, luego cargarlo dinámicamente usando JavaScript y copiarlo en su lienzo. El único inconveniente de este método son los archivos adicionales que su programa tendrá que descargar de la red, pero dependiendo del tipo de gráfico o de lo que haga su aplicación, esta podría ser una buena solución. El widget de animación utiliza este método para cargar el gráfico de la hoja giratoria, que habría sido imposible de recrear utilizando solo las funciones de trazado de la trayectoria del lienzo.

El segundo método consiste simplemente en dibujar el gráfico una vez en un lienzo fuera de la pantalla en lugar de cargar una imagen externa. Usaremos este método para almacenar en caché el título del widget de animación. Primero creamos una variable para hacer referencia al nuevo elemento canvas fuera de la pantalla que se creará. Su valor predeterminado es falso, por lo que podemos decir si se ha creado o no una memoria caché de imagen, y se guarda una vez que la secuencia de comandos comienza a ejecutarse:

Luego editamos la función drawTitle () para verificar primero si se ha creado la imagen canvas de titleCache. Si no es así, crea una imagen fuera de la pantalla y almacena una referencia a ella en titleCache:


Paso 10: borre el lienzo con clearRect ()

El primer paso para dibujar un nuevo marco de animación es borrar el lienzo del actual. Esto se puede hacer restableciendo el ancho del elemento canvas o usando la función clearRect ().

Restablecer el ancho tiene un efecto secundario de borrar también el contexto del lienzo actual a su estado predeterminado, lo que puede ralentizar las cosas. El uso de clearRect () siempre es la forma más rápida y mejor de borrar el lienzo.

En la función main (), cambiaremos esto:

...a esto:


Paso 11: Implementar capas

Si ya trabajó con software de edición de imágenes o videos como Gimp o Photoshop, entonces ya está familiarizado con el concepto de capas, donde una imagen se compone apilando muchas imágenes una encima de otra, y cada una puede seleccionarse y editado por separado.

Aplicado a una escena de animación de lienzo, cada capa será un elemento de lienzo separado, colocados uno encima del otro usando CSS para crear la ilusión de un elemento único. Como técnica de optimización, funciona mejor cuando hay una clara distinción entre los elementos en primer plano y en segundo plano de una escena, y la mayor parte de la acción tiene lugar en primer plano. El fondo se puede dibujar en un elemento de lienzo que no cambia mucho entre los cuadros de animación, y el primer plano en otro elemento de lienzo más dinámico encima de él. De esta forma, no es necesario volver a dibujar toda la escena para cada cuadro de animación.

Desafortunadamente, el widget de animación es un buen ejemplo de una escena en la que no podemos aplicar de forma útil esta técnica, ya que tanto los elementos de primer plano como los de fondo están muy animados.


Paso 12: Actualice solo las áreas cambiantes de una escena de animación

Esta es otra técnica de optimización que depende en gran medida de la composición de la escena de la animación. Se puede usar cuando la animación de la escena se concentra alrededor de una región rectangular particular en el lienzo. Entonces podríamos borrar y volver a dibujar simplemente redibujar esa región.

Por ejemplo, el título de Sintel permanece sin cambios durante la mayor parte de la animación, por lo que podríamos dejar esa área intacta al borrar el lienzo para el siguiente cuadro de animación.

Para implementar esta técnica, reemplazamos la línea que llama a la función de dibujo del título en main () con el siguiente bloque:


Paso 13: minimiza la renderización de subpíxeles

La representación de subpíxeles o antialias sucede cuando el navegador aplica automáticamente efectos gráficos para eliminar los bordes irregulares. Produce imágenes y animaciones de aspecto más suave, y se activa automáticamente cada vez que especifica coordenadas fraccionarias en lugar de números enteros cuando dibuja en el lienzo.

En este momento no existe un estándar sobre exactamente cómo se debe hacer, por lo que la representación de subpíxeles es un poco inconsistente en todos los navegadores en términos de la salida representada. También ralentiza las velocidades de renderizado ya que el navegador tiene que hacer algunos cálculos para generar el efecto. Como el suavizado de lienzos no puede desactivarse directamente, la única forma de evitarlo es usar siempre números enteros en las coordenadas de su dibujo.

Usaremos Math.floor () para asegurar números enteros en nuestro script cuando sea aplicable. Por ejemplo, la siguiente línea en drawFilm ():

... se reescribe como:


Paso 14: mide los resultados

Hemos analizado bastantes técnicas de optimización de animación de lienzo, y ahora es el momento de revisar los resultados.

Esta tabla muestra el tiempo de renderizado promedio antes y después y el FPS de Canvas. Podemos ver algunas mejoras significativas en todos los navegadores, aunque solo Chrome está cerca de lograr nuestro objetivo original de un tiempo máximo de renderización de 33 ms. Esto significa que aún queda mucho trabajo por hacer para alcanzar ese objetivo.

Podríamos continuar aplicando técnicas de optimización de JavaScript más generales, y si eso aún falla, tal vez considere atenuar la animación eliminando algunos sonidos. Pero no analizaremos ninguna de esas otras técnicas hoy en día, ya que aquí nos centramos en las optimizaciones para la animación <canvas>.

La API de Canvas todavía es bastante nueva y crece día a día, así que sigue experimentando, probando, explorando y compartiendo. Gracias por leer el tutorial.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.