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 :
<div id="animationWidget" > <canvas width="368" height="208" id="mainCanvas" ></canvas> <video width="184" height="104" id="video" autobuffer="autobuffer" controls="controls" poster="poster.jpg" > <source src="sintel.mp4" type="video/mp4" ></source> <source src="sintel.webm" type="video/webm" ></source> </video> </div>
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).
#animationWidget{ border:1px #222 solid; position:relative; width: 570px; height: 220px; } #animationWidget canvas{ border:1px #222 solid; position:absolute; top:5px; left:5px; } #animationWidget video{ position:absolute; top:110px; left:380px; }
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 quesetBlade ()
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.
(function(){ if( !document.createElement("canvas").getContext ){ return; } //the canvas tag isn't supported var mainCanvas = document.getElementById("mainCanvas"); // points to the HTML canvas element above var mainContext = mainCanvas.getContext('2d'); //the drawing context of the canvas element var video = document.getElementById("video"); // points to the HTML video element var frameDuration = 33; // the animation's speed in milliseconds video.addEventListener( 'play', init ); // The init() function is called whenever the user presses play & the video starts/continues playing video.addEventListener( 'ended', function(){ drawStats(true); } ); //drawStats() is called one last time when the video end, to sum up all the statistics var videoSamples; // this is an array of images, used to store all the snapshots of the playing video taken over time. These images are used to create the 'film reel' var backgrounds; // this is an array of images, used to store all the snapshots of the playing video taken over time. These images are used as the canvas background var blade; //An canvas element to store the image copied from blade.png var bladeSrc = 'blade.png'; //path to the blade's image source file var lastPaintCount = 0; // stores the last value of mozPaintCount sampled var paintCountLog = []; // an array containing all measured values of mozPaintCount over time var speedLog = []; // an array containing all the execution speeds of main(), measured in milliseconds var fpsLog = []; // an array containing the calculated frames per secong (fps) of the script, measured by counting the calls made to main() per second var frameCount = 0; // counts the number of times main() is executed per second. var frameStartTime = 0; // the last time main() was called // Called when the video starts playing. Sets up all the javascript objects required to generate the canvas animation and measure perfomance function init(){ if( video.currentTime > 1 ){ return; } bladeSrc = new Image(); bladeSrc.src = "blade.png"; bladeSrc.onload = setBlade; backgrounds = []; videoSamples = []; fpsLog = []; paintCountLog = []; if( window.mozPaintCount ){ lastPaintCount = window.mozPaintCount; } speedLog = []; frameCount = 0; frameStartTime = 0; main(); setTimeout( getStats, 1000 ); } // As the scripts main function, it controls the pace of the animation function main(){ setTimeout( main, frameDuration ); if( video.paused || video.ended ){ return; } var now = new Date().getTime(); if( frameStartTime ){ speedLog.push( now - frameStartTime ); } frameStartTime = now; if( video.readyState < 2 ){ return; } frameCount++; mainCanvas.width = mainCanvas.width; //clear the canvas drawBackground(); drawFilm(); drawDescription(); drawStats(); drawBlade(); drawTitle(); } // This function is called every second, and it calculates and stores the current frame rate function getStats(){ if( video.readyState >= 2 ){ if( window.mozPaintCount ){ //this property is specific to firefox, and tracks how many times the browser has rendered the window since the document was loaded paintCountLog.push( window.mozPaintCount - lastPaintCount ); lastPaintCount = window.mozPaintCount; } fpsLog.push(frameCount); frameCount = 0; } setTimeout( getStats, 1000 ); } // create blade, the ofscreen canavs that will contain the spining animation of the image copied from blade.png function setBlade(){ blade = document.createElement("canvas"); blade.width = 400; blade.height = 400; blade.angle = 0; blade.x = -blade.height * 0.5; blade.y = mainCanvas.height/2 - blade.height/2; } // Creates and returns a new image that contains a snapshot of the currently playing video. function sampleVideo(){ var newCanvas = document.createElement("canvas"); newCanvas.width = video.width; newCanvas.height = video.height; newCanvas.getContext("2d").drawImage( video, 0, 0, video.width, video.height ); return newCanvas; } // renders the dark background for the whole canvas element. The background features a greyscale sample of the video and a gradient overlay function drawBackground(){ var newCanvas = document.createElement("canvas"); var newContext = newCanvas.getContext("2d"); newCanvas.width = mainCanvas.width; newCanvas.height = mainCanvas.height; newContext.drawImage( video, 0, video.height * 0.1, video.width, video.height * 0.5, 0, 0, mainCanvas.width, mainCanvas.height ); var imageData, data; try{ imageData = newContext.getImageData( 0, 0, mainCanvas.width, mainCanvas.height ); data = imageData.data; } catch(error){ // CORS error (eg when viewed from a local file). Create a solid fill background instead newContext.fillStyle = "yellow"; newContext.fillRect( 0, 0, mainCanvas.width, mainCanvas.height ); imageData = mainContext.createImageData( mainCanvas.width, mainCanvas.height ); data = imageData.data; } //loop through each pixel, turning its color into a shade of grey for( var i = 0; i < data.length; i += 4 ){ var red = data[i]; var green = data[i + 1]; var blue = data[i + 2]; var grey = Math.max( red, green, blue ); data[i] = grey; data[i+1] = grey; data[i+2] = grey; } newContext.putImageData( imageData, 0, 0 ); //add the gradient overlay var gradient = newContext.createLinearGradient( mainCanvas.width/2, 0, mainCanvas.width/2, mainCanvas.height ); gradient.addColorStop( 0, '#000' ); gradient.addColorStop( 0.2, '#000' ); gradient.addColorStop( 1, "rgba(0,0,0,0.5)" ); newContext.fillStyle = gradient; newContext.fillRect( 0, 0, mainCanvas.width, mainCanvas.height ); mainContext.save(); mainContext.drawImage( newCanvas, 0, 0, mainCanvas.width, mainCanvas.height ); mainContext.restore(); } // renders the 'film reel' animation that scrolls across the canvas function drawFilm(){ var sampleWidth = 116; // the width of a sampled video frame, when painted on the canvas as part of a 'film reel' var sampleHeight = 80; // the height of a sampled video frame, when painted on the canvas as part of a 'film reel' var filmSpeed = 20; // determines how fast the 'film reel' scrolls across the generated canvas animation. var filmTop = 120; //the y co-ordinate of the 'film reel' animation var filmAngle = -10 * Math.PI / 180; //the slant of the 'film reel' var filmRight = ( videoSamples.length > 0 )? videoSamples[0].x + videoSamples.length * sampleWidth : mainCanvas.width; //the right edge of the 'film reel' in pixels, relative to the canvas //here, we check if the first frame of the 'film reel' has scrolled out of view if( videoSamples.length > 0 ){ var bottomLeftX = videoSamples[0].x + sampleWidth; var bottomLeftY = filmTop + sampleHeight; bottomLeftX = Math.floor( Math.cos(filmAngle) * bottomLeftX - Math.sin(filmAngle) * bottomLeftY ); // the final display position after rotation if( bottomLeftX < 0 ){ //the frame is offscreen, remove it's refference from the film array videoSamples.shift(); } } // add new frames to the reel as required while( filmRight <= mainCanvas.width ){ var newFrame = {}; newFrame.x = filmRight; newFrame.canvas = sampleVideo(); videoSamples.push(newFrame); filmRight += sampleWidth; } // create the gradient fill for the reel var gradient = mainContext.createLinearGradient( 0, 0, mainCanvas.width, mainCanvas.height ); gradient.addColorStop( 0, '#0D0D0D' ); gradient.addColorStop( 0.25, '#300A02' ); gradient.addColorStop( 0.5, '#AF5A00' ); gradient.addColorStop( 0.75, '#300A02' ); gradient.addColorStop( 1, '#0D0D0D' ); mainContext.save(); mainContext.globalAlpha = 0.9; mainContext.fillStyle = gradient; mainContext.rotate(filmAngle); // loops through all items of film array, using the stored co-ordinate values of each to draw part of the 'film reel' for( var i in videoSamples ){ var sample = videoSamples[i]; var punchX, punchY, punchWidth = 4, punchHeight = 6, punchInterval = 11.5; //draws the main rectangular box of the sample mainContext.beginPath(); mainContext.moveTo( sample.x, filmTop ); mainContext.lineTo( sample.x + sampleWidth, filmTop ); mainContext.lineTo( sample.x + sampleWidth, filmTop + sampleHeight ); mainContext.lineTo( sample.x, filmTop + sampleHeight ); mainContext.closePath(); //adds the small holes lining the top and bottom edges of the 'fim reel' for( var j = 0; j < 10; j++ ){ punchX = sample.x + ( j * punchInterval ) + 5; punchY = filmTop + 4; mainContext.moveTo( punchX, punchY + punchHeight ); mainContext.lineTo( punchX + punchWidth, punchY + punchHeight ); mainContext.lineTo( punchX + punchWidth, punchY ); mainContext.lineTo( punchX, punchY ); mainContext.closePath(); punchX = sample.x + ( j * punchInterval ) + 5; punchY = filmTop + 70; mainContext.moveTo( punchX, punchY + punchHeight ); mainContext.lineTo( punchX + punchWidth, punchY + punchHeight ); mainContext.lineTo( punchX + punchWidth, punchY ); mainContext.lineTo( punchX, punchY ); mainContext.closePath(); } mainContext.fill(); } //loop through all items of videoSamples array, update the x co-ordinate values of each item, and draw its stored image onto the canvas mainContext.globalCompositeOperation = 'lighter'; for( var i in videoSamples ){ var sample = videoSamples[i]; sample.x -= filmSpeed; mainContext.drawImage( sample.canvas, sample.x + 5, filmTop + 10, 110, 62 ); } mainContext.restore(); } // renders the canvas title function drawTitle(){ mainContext.save(); mainContext.fillStyle = 'black'; mainContext.fillRect( 0, 0, 368, 25 ); mainContext.fillStyle = 'white'; mainContext.font = "bold 21px Georgia"; mainContext.fillText( "SINTEL", 10, 20 ); mainContext.restore(); } // renders all the text appearing at the top left corner of the canvas function drawDescription(){ var text = []; //stores all text items, to be displayed over time. the video is 60 seconds, and each will be visible for 10 seconds. text[0] = "Sintel is an independently produced short film, initiated by the Blender Foundation."; text[1] = "For over a year an international team of 3D animators and artists worked in the studio of the Amsterdam Blender Institute on the computer-animated short 'Sintel'."; text[2] = "It is an epic short film that takes place in a fantasy world, where a girl befriends a baby dragon."; text[3] = "After the little dragon is taken from her violently, she undertakes a long journey that leads her to a dramatic confrontation."; text[4] = "The script was inspired by a number of story suggestions by Martin Lodewijk around a Cinderella character (Cinder in Dutch is 'Sintel'). "; text[5] = "Screenwriter Esther Wouda then worked with director Colin Levy to create a script with multiple layers, with strong characterization and dramatic impact as central goals."; text = text[Math.floor( video.currentTime / 10 )]; //use the videos current time to determine which text item to display. mainContext.save(); var alpha = 1 - ( video.currentTime % 10 ) / 10; mainContext.globalAlpha = ( alpha < 5 )? alpha : 1; mainContext.fillStyle = '#fff'; mainContext.font = "normal 12px Georgia"; //break the text up into several lines as required, and write each line on the canvas text = text.split(' '); var colWidth = mainCanvas.width * .75; var line = ''; var y = 40; for(var i in text ){ line += text[i] + ' '; if( mainContext.measureText(line).width > colWidth ){ mainContext.fillText( line, 10, y ); line = ''; y += 12; } } mainContext.fillText( line, 10, y ); mainContext.restore(); } //updates the bottom-right potion of the canvas with the latest perfomance statistics function drawStats( average ){ var x = 245.5, y = 130.5, graphScale = 0.25; mainContext.save(); mainContext.font = "normal 10px monospace"; mainContext.textAlign = 'left'; mainContext.textBaseLine = 'top'; mainContext.fillStyle = 'black'; mainContext.fillRect( x, y, 120, 75 ); //draw the x and y axis lines of the graph y += 30; x += 10; mainContext.beginPath(); mainContext.strokeStyle = '#888'; mainContext.lineWidth = 1.5; mainContext.moveTo( x, y ); mainContext.lineTo( x + 100, y ); mainContext.stroke(); mainContext.moveTo( x, y ); mainContext.lineTo( x, y - 25 ); mainContext.stroke(); // draw the last 50 speedLog entries on the graph mainContext.strokeStyle = '#00ffff'; mainContext.fillStyle = '#00ffff'; mainContext.lineWidth = 0.3; var imax = speedLog.length; var i = ( speedLog.length > 50 )? speedLog.length - 50 : 0 mainContext.beginPath(); for( var j = 0; i < imax; i++, j += 2 ){ mainContext.moveTo( x + j, y ); mainContext.lineTo( x + j, y - speedLog[i] * graphScale ); mainContext.stroke(); } // the red line, marking the desired maximum rendering time mainContext.beginPath(); mainContext.strokeStyle = '#FF0000'; mainContext.lineWidth = 1; var target = y - frameDuration * graphScale; mainContext.moveTo( x, target ); mainContext.lineTo( x + 100, target ); mainContext.stroke(); // current/average speedLog items y += 12; if( average ){ var speed = 0; for( i in speedLog ){ speed += speedLog[i]; } speed = Math.floor( speed / speedLog.length * 10) / 10; }else { speed = speedLog[speedLog.length-1]; } mainContext.fillText( 'Render Time: ' + speed, x, y ); // canvas fps mainContext.fillStyle = '#00ff00'; y += 12; if( average ){ fps = 0; for( i in fpsLog ){ fps += fpsLog[i]; } fps = Math.floor( fps / fpsLog.length * 10) / 10; }else { fps = fpsLog[fpsLog.length-1]; } mainContext.fillText( ' Canvas FPS: ' + fps, x, y ); // browser frames per second (fps), using window.mozPaintCount (firefox only) if( window.mozPaintCount ){ y += 12; if( average ){ fps = 0; for( i in paintCountLog ){ fps += paintCountLog[i]; } fps = Math.floor( fps / paintCountLog.length * 10) / 10; }else { fps = paintCountLog[paintCountLog.length-1]; } mainContext.fillText( 'Browser FPS: ' + fps, x, y ); } mainContext.restore(); } //draw the spining blade that appears in the begining of the animation function drawBlade(){ if( !blade || blade.x > mainCanvas.width ){ return; } blade.x += 2.5; blade.angle = ( blade.angle - 45 ) % 360; //update blade, an ofscreen canvas containing the blade's image var angle = blade.angle * Math.PI / 180; var bladeContext = blade.getContext('2d'); blade.width = blade.width; //clear the canvas bladeContext.save(); bladeContext.translate( 200, 200 ); bladeContext.rotate(angle); bladeContext.drawImage( bladeSrc, -bladeSrc.width/2, -bladeSrc.height/2 ); bladeContext.restore(); mainContext.save(); mainContext.globalAlpha = 0.95; mainContext.drawImage( blade, blade.x, blade.y + Math.sin(angle) * 50 ); mainContext.restore(); } })();
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.
var frameDuration = 33; // set the animation's target speed in milliseconds function main(){ if( video.paused || video.ended ){ return false; } setTimeout( main, frameDuration );
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:
var lastPaintCount = window.mozPaintCount; setInterval( function(){ console.log( window.mozPaintCount - lastPaintCount ); lastPaintCount = window.mozPaintCount; }, 1000);
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:
requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || setTimeout;
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:
setTimeout( main, frameDuration );
...a:
requestAnimationFrame( main, canvas );
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:
document.addEventListener( 'visibilitychange', onVisibilityChange, false);
Luego viene la función del controlador de eventos:
// Adjusts the program behavior, based on whether the webpage is active or hidden function onVisibilityChange() { if( document.hidden && !video.paused ){ video.pause(); }else if( video.paused ){ video.play(); } }
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:
mainContext.beginPath(); for( var j = 0; i < imax; i++, j += 2 ){ mainContext.moveTo( x + j, y ); // define the subpaths starting point mainContext.lineTo( x + j, y - speedLog[i] * graphScale ); // set the subpath as a line, and define its endpoint mainContext.stroke(); // draw the subpath to the canvas }
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.
mainContext.beginPath(); for( var j = 0; i < imax; i++, j += 2 ){ mainContext.moveTo( x + j, y ); // define the subpaths starting point mainContext.lineTo( x + j, y - speedLog[i] * graphScale ); // set the subpath as a line, and define its endpoint } mainContext.stroke(); // draw the whole current path to the mainCanvas.
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.
var mainCanvas = document.getElementById("mainCanvas"); // points to the on-screen, original HTML canvas element var mainContext = mainCanvas.getContext('2d'); // the drawing context of the on-screen canvas element var osCanvas = document.createElement("canvas"); // creates a new off-screen canvas element var osContext = osCanvas.getContext('2d'); //the drawing context of the off-screen canvas element osCanvas.width = mainCanvas.width; // match the off-screen canvas dimensions with that of #mainCanvas osCanvas.height = mainCanvas.height;
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.
// As the scripts main function, it controls the pace of the animation function main(){ requestAnimationFrame( main, mainCanvas ); if( video.paused || video.currentTime > 59 ){ return; } var now = new Date().getTime(); if( frameStartTime ){ speedLog.push( now - frameStartTime ); } frameStartTime = now; if( video.readyState < 2 ){ return; } frameCount++; osCanvas.width = osCanvas.width; //clear the offscreen canvas drawBackground(); drawFilm(); drawDescription(); drawStats(); drawBlade(); drawTitle(); mainContext.drawImage( osCanvas, 0, 0 ); // copy the off-screen canvas graphics to the on-screen canvas }
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:
var titleCache = false; // points to an off-screen canvas used to cache the animation scene's title
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
:
// renders the canvas title function drawTitle(){ if( titleCache == false ){ // create and save the title image titleCache = document.createElement('canvas'); titleCache.width = osCanvas.width; titleCache.height = 25; var context = titleCache.getContext('2d'); context.fillStyle = 'black'; context.fillRect( 0, 0, 368, 25 ); context.fillStyle = 'white'; context.font = "bold 21px Georgia"; context.fillText( "SINTEL", 10, 20 ); } osContext.drawImage( titleCache, 0, 0 ); }
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:
osCanvas.width = osCanvas.width; //clear the off-screen canvas
...a esto:
osContext.clearRect( 0, 0, osCanvas.width, osCanvas.height ); //clear the offscreen canvas
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:
if( titleCache == false ){ // If titleCache is false, the animation's title hasn't been drawn yet drawTitle(); // we draw the title. This function will now be called just once, when the program starts osContext.rect( 0, 25, osCanvas.width, osCanvas.height ); // this creates a path covering the area outside by the title osContext.clip(); // we use the path to create a clipping region, that ignores the title's region }
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 ()
:
punchX = sample.x + ( j * punchInterval ) + 5; // the x co-ordinate
... se reescribe como:
punchX = sample.x + ( j * punchInterval ) + 5; // the x co-ordinate
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.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weekly