soy Kseso y esto EsCSS

Zootropismo: Espías Javascript de eventos para animaciones CSS

Nueva entrega de la serie Javascript con Furoya en esta ocasión dedicada a cómo saber con Javascript cuándo comienza y cuándo termina una animación CSS para poder actuar en base a ello y entre otros usos poder realizar animaciones aleatorias y/o encadenadas entre distintos elementos.

Zootropismo: Espías Javascript de eventos para animaciones CSS

✎ 1
COLABORACIÓN AUTOR INVITADO
Zootropismo: Escuchadores de Eventos para Animaciones CSS

Atendiendo al éxito arrollador que tuvo el Colores aleatorios con javascript artículo publicado a fines del 2015, voy a seguir dando más vueltas a las animaciones de CSS forzadas con javascript.

En aquel momento hablamos de una coincidencia entre transition-duration y el tiempo para repetir una función javascript.

Parecía evidente y obligada la espera a que una transición se completara antes de que con un setTimeout se reescribiera un valor CSS que empezara otra vez la animación. Aunque también comentamos que si el escript se adelantaba, solamente la iba a interrumpir forzando a un nuevo cambio que dará una impresión más caótica del efecto. Algo que en muchos casos puede ser justo lo que buscamos.

Para lograr aquella coincidencia de tiempos de la que hablábamos, se usó un método algo rebuscado, pero ya conocido de otros artículos : la captura del valor transition-duration en la hoja de estilos que luego se pasó al setTimeout de javascript. Esto fue para no presentar entonces un evento que sí vamos a ver hoy en más detalle (porque en el resumen del Primer Año de mis colaboraciones sólo lo mostramos) y que simplifica enormemente nuestro trabajo (aunque por otro lado, también lo complica).

Cuando la transición o la animación se terminan, disparan siempre un evento javascript para CSS llamado transitionend, o animationend (según el caso). Lamentablemente, este evento no se puede escribir como atributo "manejador de eventos" dentro de una etiqueta, y sólo se asigna a través de un "escuchador de eventos" sobre el elemento que tenga la transición.

Y addEventListener(evento, objeto a ejecutar, [booleano fase de propagación]) tiene reglas algo particulares que aún no vimos, pero en algún momento hay que familiarizarse con ellas porque es un método que vamos a usar muy a menudo.

Molestando animales virtuales.

Para no hacer cambios de ejemplo tan bruscos, vamos a seguir con las animaciones de colores. Esta vez la vamos a disparar nosotros con un click en algún elemento, y vamos a lograr que ese evento cambie la opacidad de su hermano siguiente, y que al terminar su animación se ejecute una función que haga lo mismo con el que le sigue. Y al llegar al último que recomience con el primero.

Estos elementos hermanos los escribimos con javascript, y les damos los colores a cada uno para que al estar juntos muestren un gradiente. "Detrás" (como fondo de su contenedor) hay un verdadero gradiente con otro orden de colores. Al transparentarse cada capa coloreada deja ver ese color de fondo, y como la animación está secuenciada, el cambio de colores parece desplazarse.

Es algo como esto:

See the Pen Jibia animada. by solipsistaCP (@solipsistacp) on CodePen.

Imagen de www.calamarpedia.com

Al pasar el puntero sobre la jibia se abre un panel que muestra las capas con los colores. Haciendo un click en la primera (roja, a la izquierda) se transparenta la segunda, al terminar sigue con la tercera. Cuando termina con todas, recomienza la serie, pero ahora opacándolas.

Antes de mencionar un detalle importante, veamos el código comentado.

<script type="text/javascript"> var bandas; //VARIABLE GLOBAL /* CREA CAPAS CON BANDAS DE COLORES */ function rellena() { /* REFIERE AL ELEMENTO CONTENEDOR */ caja = document.querySelector("#caja"); /* CAPTURA SU ANCHO */ anchoCaja = caja.offsetWidth; /* CALCULA EL ANCHO DE CADA BANDA DE COLOR PARA ESCRIBIR LAS 28 */ anchoBandas = Math.ceil(anchoCaja / 28); /* CALCULA LOS GRADOS DE HUE PARA 28 BANDAS */ var girocolor = 360 / 28; /* CREA LAS 28 BANDAS */ for(b=0; b<28; b++) { /* CREA UN VALOR DE COLOR */ nuevoColor = "hsl("+ (b*girocolor) +", 100%, 50%)"; /* CREA UN DIV */ unaBanda = document.createElement("div"); /* LE PONE UNA CLASE */ unaBanda.setAttribute("class","bandas"); /* LE ASIGNA EL ANCHO CALCULADO */ unaBanda.style.width = anchoBandas+"px"; /* Y EL VALOR DE COLOR COMO FONDO */ unaBanda.style.backgroundColor = nuevoColor; /* TAMBIEN COMO SOMBRA */ unaBanda.style.boxShadow = "-"+anchoBandas/2+"px 0 "+anchoBandas/2+"px 0 "+ nuevoColor; /* Y LO AGREGA A LA CAJA */ caja.appendChild(unaBanda); } /* LISTA TODOS LOS ELEMENTOS QUE SON BANDAS DE COLOR */ var lasBandas = document.querySelectorAll(".bandas"); /* LES ASIGNA EL EVENTO transitionend Y UNA FUNCIÓN */ for (i=0; i<lasBandas.length; i++){ lasBandas[i].addEventListener("transitionend", function(){ muestra(this); }, false); } /* AL PRIMER ELEMENTO LE ASIGNA ADEMÁS UN EVENTO click */ lasBandas[0].addEventListener("click", function(){ muestra(this); }, false); } /* FUNCIÓN QUE TRANSPARENTA BANDAS DE COLOR PARA MOSTRAR EL GRADIENTE DE FONDO */ function muestra(T) { /* SI EXISTE UN ELEMENTO HERMANO SIGUIENTE ... */ if(T.nextElementSibling) { /* ... LE CAMBIA LA OPACIDAD, ... */ T.nextElementSibling.style.opacity = (T.nextElementSibling.style.opacity == "0")? "1" : "0"; } /* ... SI NO (PORQUE ES EL ÚLTIMO) CAMBIA LA OPACIDAD DEL PRIMERO */ else { T.parentElement.querySelectorAll(".bandas")[0].style.opacity = (T.parentElement.querySelectorAll(".bandas")[0].style.opacity == "0")? "1" : "0"; } } onload = rellena; </script>

La jibia es una imagen semitransparente que deja ver las capas y los gradientes de fondo que tiene detrás, y el detalle de la composición es el siguiente:

  • La caja contenedora tiene un gradiente de fondo CSS y una imagen dentro. En medio de estos, se crean unas capas a modo de bandas verticales (28, para el caso) y se les dan colores simulando un degradado tipo "arcoiris".
  • Para calcular el ancho de cada banda, se divide el ancho del contenedor entre 28. Si el número queda con decimales, se redondea para arriba con Math.ceil()
  • El color de cada una se calcula a partir de los 360° del hue de hsl(). Sólo hay que dividir el círculo entre 28 para obtener su valor en grados. (Como éste es un blog de CSS y ya todos saben qué es un color HSL o HSLA, no me detengo en más detalles).
  • Un bucle for() sirve para escribir sus códigos.
    • El color se consigue multiplicando el valor unitario en grados por la variable incremental.
    • Cada banda es un div escrito al vuelo. Y se le da una misma clase para que tengan el mismo formato de base.
    • El ancho es igual para todas, pero como hubo que calcularlo con javascript y es medio complicado agregarlo a la hoja de estilo, se lo ponemos a cada una como inline, junto con el color previamente creado. Y una sombra con ese mismo color para suavizar visualmente la juntura de las capas.
    • A medida que se termina de escribir un elemento, se lo agrega al contenedor.
  • Una vez ubicadas todas las bandas de color, se las 'pone' en una variable como list-item y se las recorre con otro bucle for() para asignarles el escuchador de evento de fin de transición. Éste dispara una función con un argumento this que vamos a describir más abajo.
  • Al primer elemento (la banda roja de la izquierda) también le asignamos un event listener para el click. Se podría haber hecho con setAttribute, pero ya que estamos les muestro cómo hacerlo así.
  • La función muestra(T) se ejecuta cuando se hace un click en el primer color, y cuando se termina el tiempo de transición en cada div.
  • Si el elemento donde se produce el evento tiene un elemento hermano siguiente (lo sabemos con nextElementSibling) le cambia la opacidad, que como tiene una transición CSS, va a hacer lo mismo con la siguiente a través de su propio evento transitionend.
  • Cuando llegue al último y a continuación ya no tenga más hermanos (por lo que T.nextElementSibling va a devolver false), entonces va a ir a modificar al primero y así recomenzar la serie infinitamente.

Una vez comprendido el mecanismo básico del efecto, empezamos a acercarnos al comentario sobre animaciones caóticas mencionado al principio. Si en medio de la secuencia clickamos otra vez el botón rojo, vamos a comenzar otra serie a continuación de la que ya tenemos.

Y si seguimos clickando en algún momento vamos a alterar el tiempo de transición superponiendo otro de algún evento anterior. Porque ahora los manejamos nosotros y así los hacemos parecer aleatorios.

Apuntes básicos sobre addEventListener()

Estuve evitando este método (addEventListener()) a lo largo de estos artículos todo lo que me fue posible. La razón: tiene algunas características que lo hacen javascript avanzado y se salen de los usos que le podemos dar aquí. Pero cada vez está más difundido y hay que aprender a manejarse con él.

Existiendo montones de tutoriales con descripciones detalladas, no voy a poner uno más. Sí voy a mencionar algo de lo que se usa en estos ejemplos para que tengan como referencia al buscar documentación.

En realidad esta función no pone un evento —o un atributo con el evento— a un elemento cualquiera. Lo que hace es monitorear si algún evento se produce y coincide con su primer argumento. La ventaja es que se puede usar el mismo en varios addEventListener y no van a interferir entre ellos.

Los problemas empiezan en el segundo argumento que solamente puede ser un objeto. En la práctica será una función anónima, o una referencia a la función por su nombre o por su variable si estuviese contenida en una. En ejemplos como el anterior nos limita enormemente para poner una función con sus propios argumentos, como por ejemplo this, para hacer referencia al elemento donde se produce el evento, porque ya dijimos que no pone el "detector de evento" allí, y entonces el this va a buscar su referencia dentro del "escuchador", que es donde realmente está.

Eso es lo que pasa en nuestro ejemplo previo. Necesitamos que la función muestra(T) capture en su argumento T una referencia al elemento donde ocurre el evento (lo que normalmente haría this), referencia que para nosotros cambia en cada vuelta porque es su 'punto' para encontrar al hermano siguiente y modificarlo.

El truco para conseguirlo no está explicado en el desarrollo que dejé arriba, y lo hago ahora:
Estamos usando funciones anidadas. La primera —que debe ser anónima para que cumpla con las condiciones de funcionamiento del método— contiene a la función que sí se va a ejecutar en el contexto del elemento, y su argumento para this va a estar apuntando allí. Es algo como una función encapsulada, y así la aísla del "escuchador"; si quisiéramos pasarle argumentos de su propio escript habría que hacerlo a través de parámetros en la función anónima, usándola como un "relevador". O con la ayuda de otros métodos, como call() o bind(), según el caso.

Pero resulta que éste modo no es el más profesional de hacerlo. Hay otra forma de la que les voy a presentar dos ejemplos más abajo.

Ahora sí ¡seguimos jodiendo bichos!

Hasta el momento hemos trabajado con colores, y por supuesto que no es lo único que se puede animar. El siguiente proyecto ya no usa elementos adyacentes o hermanos sino hijos o elementos enlazados: el primero contiene al segundo, que contiene al tercero, que contiene al cuarto...

De esta forma, el contenedor principal en realidad contiene a todos, pero no como hijos directos, sino como hijo, nieto, bisnieto, ...

Ya vimos estructuras similares en el artículo Pseudo borde circular de caracteres tipográficos, y lo vamos a usar para crearlas un escript explicado en ese artículo.

La diferencia de comportamiento está en que una animación del primer ancestro va a arrastrar a todos sus descendientes, pero la del segundo solamente a los suyos y ya no al padre; lo que crea un efecto visual de movimiento en "ondas encadenadas".

En el siguiente ejemplo, las ondas van a estar interferidas unas con otras porque la idea es que se modifiquen entre ellas, pero vamos a inventar algo para que les puedan seguir el movimiento.

See the Pen Escolopendra animada. (1) by solipsistaCP (@solipsistacp) on CodePen.

Haciendo un click en la cola de la escolopendra, ésta comienza a retorcerse. (Imagen de Internet Archive Book Images.)

En realidad no es una imagen, sino una veintena ubicadas con posición absoluta para reconstruir el dibujo. Cada una está como fondo de un span, que están anidados tal como expliqué arriba. El click inicia la animación en su padre directo, que al terminar dispara la animación en el suyo. Ésta es una propagación forzada por el escript que se detiene en el primer contenedor del bicho (la cabeza) con un condicional (se los explico mejor después), pero resulta que hay una propagación natural de los eventos, que me parece no comentamos nunca.

Ocurre que al hacer un click (dblclick, mouseover, o cualquier otro) en un elemento, también lo estamos haciendo en su contenedor, y en el de éste; y así hasta llegar al <html>. Si hubiese otro evento para el click en un ancestro, se dispararían ambos.

Como aquí hay nada más que uno, ni lo notamos. Pero resulta que el transitionend también se propaga; y peor aún, por otra de las particularidades de addEventListener la propagación se puede iniciar en el primer ancestro ... y bajar chocando con la que viene subiendo por nuestra función (esa dirección de fase se controla con el último argumento booleano, lo mencioné a la pasada arriba y lo pueden estudiar en algún tutorial como el que linkeo abajo, con sus propios ejemplos); y todavía peor aún, si repetimos el click vamos a enviar una o más ondas que van a terminar haciendo una sacudida absolutamente caótica del pobre bicho con los choques de onda que van y vienen. Y que es lo que estábamos buscando para el efecto.

Veamos el escript comentado. La parte más larga es para construir el dibujo, la más compleja para animarlo:

<script type="text/javascript"> var caja; // VARIABLE GLOBAL PARA CONTENEDOR /* ALTURAS DE CADA IMAGEN EN ORDEN */ var altos = [63,19,17,21,23,22,25,26,29,32,38,41,41,42,41,36,35,40,92]; /* top DE CADA CONTENEDOR DE IMAGEN EN ORDEN */ var topes = [0,53,15,12,12,11,10,12,11,13,12,15,13,14,15,13,12,11,12]; /* CREA Y UBICA LOS SEGMENTOS DE IMAGEN */ function inicia() { /* ASIGNA A VARIABLE EL CONTENEDOR */ caja = document.querySelector("#cajaEntomologica"); /* LO LLENA CON LOS span ANIDADOS */ caja.innerHTML = Array(altos.length+1).join("<span>") + Array(altos.length+1).join("</span>"); /* RECORRE LOS span CREADOS */ for(i=0; i<altos.length; i++) { /* UBICA A CADA UNO */ elSegmento = document.querySelectorAll("#cajaEntomologica span")[i]; /* ASIGNA UN IDENTIFICADOR, ... */ elSegmento.id = "anillo"+i; /* ... SU IMAGEN DE FONDO, ... */ elSegmento.style.background = "url(https://e1140ac37cfe956d7b5657ebb2a7d4e001ade301.googledrive.com/host/0Bwv59jgRLbvUelpIdnpCY2F5WXc/segEscolopendra"+i+".png) 0 0 no-repeat"; /* SU ALTURA, ... */ elSegmento.style.height = altos[i]+"px"; /* SU POSICIÓN, ... */ elSegmento.style.top = topes[i]+"px"; /* ... Y LE ASIGNA UN EVENTO transitionend QUE DISPARA UNA FUNCIÓN */ elSegmento.addEventListener("transitionend", function(){ mueve(this)}, false); } /* REFERENCIA AL ÚLTIMO span */ var ultimoSegmento = document.querySelectorAll("#cajaEntomologica span")[altos.length-1]; /* LE ASIGNA UN EVENTO click QUE TAMBIÉN DISPARA LA FUNCIÓN */ ultimoSegmento.addEventListener("click", function(){ mueve(this)}, false); /* Y UN ESTILO DE PUNTERO MANECILLA */ ultimoSegmento.style.cursor = "pointer"; /* AL PRIMER span LE ASIGNA UNA CLASE */ document.querySelectorAll("#cajaEntomologica span")[0].className = "arriba"; } /* CAMBIA ÁNGULOS */ function mueve(T) { //console.log(T+" "+T.id); /* SI NO ES EL PRIMER ELEMENTO DE LOS ANIDADOS ... */ if(T.parentElement.className != "arriba") { /* ... CAMBIA EL ÁNGULO AL AZAR DENTRO DE UN RANGO DE +/- 16deg */ T.parentElement.style.transform = "rotate("+ Math.floor((Math.random()*16)-8) +"deg)"; } } /* EJECUTA LA FUNCIÓN AL CARGAR EL DOCUMENTO */ onload = inicia; </script>

Esto parece más arduo, pero es nada más que por el armado del bicho, que tiene partes de diferentes tamaños y posiciones. Una vez acomodados, solamente se les aplica una transición rotate en secuencia para que simulen un movimiento de segmentos en el cuerpo del animal. Y como dije, la forma de hacerlo requiere un poco de atención para comprenderla:

  • Lo que se destaca entre las variables iniciales son dos arrays con valores que no se entiende de dónde salen. Y es porque están escritos "a mano". Dependen de la posición y tamaño de cada span, que a su vez dependen del diseño de las partes de la imagen. Por eso hay que ir creando la composición en un documento y anotar cada valor de top y height para los arrays, y en el documento final los usamos para reconstruir la imagen tal como la hicimos en el borrador. Todo esto para no escribirla en el código fuente y hacer que la meta un escript. =P
  • La función de inicio crea todas las cajas anidadas calculando sobre el total de elementos de array, y las pone en el elemento contenedor que ya está en el documento.
  • Después las recorre con un bucle for() para agregarles un identificador numerado, una imagen de fondo que también está previamente numerada en el nombre, la altura y el top que le corresponde según los arrays altos y topes y el escuchador de eventos con el parámetro final true.
    [Nota: como dije, hay montones de tutoriales con la explicación de cómo trabaja éste booleano, pero les dejo una que encontré y me parece de las más claras El flujo de los eventos: captura y burbuja.]
  • Para terminar la escolopendra, a su cola se le asigna el puntero manecilla y un evento click que dispara la misma función que las transiciones.
  • La función que cambia los ángulos toma el argumento this que va a ser el span donde se lea el evento disparado con el ratón o por la transición CSS (todos apuntan a esta misma función) y con esta referencia encuentra a su ancestro inmediato usando parentElement para cambiar el estilo de transformación con un nuevo valor aleatorio que es el que va a ver la hoja de estilos para empezar la transición. Un condicional evita que al llegar a la cabeza se siga aplicando el transform:rotate(), y allí se detiene.
  • La forma en que se propaga el movimiento hacia arriba desde la cola, y hacia abajo desde el contenedor principal hace que las animaciones se choquen y hagan un movimiento cortado. Esto disminuye las posibilidades de un retorcimiento exagerado en los segmentos (los puntos extremos están en +/-8 grados), pero a medida que sube, cada caja rota a todas sus hijas; así que siendo los valores al azar, puede que la sacudida sea poco realista.
  • Para estudiar mejor cómo los eventos se van repitiendo cada vez menos veces a medida que suben por el DOM, hay una linea (comentada) que muestra el registro en la consola (se abre con la tecla F12, y se habilita el registro javascript de consola). Allí aparecen el tipo de objeto donde se produce el evento y a continuación su identificador (¡para eso les pusimos un id!). Como la propagación es un poco violenta (la del navegador, no la del CSS que tiene un delay de dos segundos y medio) la consola no muestra todos los eventos sino que numera las repeticiones a un costado. Ahí vemos cómo se van repitiendo menos veces a medida que avanzan los segmentos. O también puede ser que la consola se cuelgue y ponga un cartelito de "...Y chorrofientos mensajes más..." mostrando nada más que los primeros. O que se cuelgue javascript en el navegador y se clave después de las primeras transiciones. Es porque estos efectos consumen muchos recursos.

Otra versión del efecto anterior, sin meter funciones en funciones

Esto es un poco más limpio, y usa una de esas características raras de addEventListener de las que hablábamos más arriba. Primero les dejo el pen.

See the Pen Escolopendra animada. (2) by solipsistaCP (@solipsistacp) on CodePen.

Y este el escript comentado:

<script type="text/javascript"> var caja; // VARIABLE GLOBAL PARA CONTENEDOR /* ALTURAS DE CADA IMAGEN EN ORDEN */ var altos = [63,19,17,21,23,22,25,26,29,32,38,41,41,42,41,36,35,40,92]; /* top DE CADA CONTENEDOR DE IMAGEN EN ORDEN */ var topes = [0,53,15,12,12,11,10,12,11,13,12,15,13,14,15,13,12,11,12]; /* CREA Y UBICA LOS SEGMENTOS DE IMAGEN */ function inicia() { /* ASIGNA A VARIABLE EL CONTENEDOR */ caja = document.querySelector("#cajaEntomologica"); /* LO LLENA CON LOS span ANIDADOS */ caja.innerHTML = Array(altos.length+1).join("<span>") + Array(altos.length+1).join("</span>"); /* RECORRE LOS span CREADOS */ for(i=0; i<altos.length; i++) { /* UBICA A CADA UNO */ elSegmento = document.querySelectorAll("#cajaEntomologica span")[i]; /* ASIGNA UN IDENTIFICADOR, ... */ elSegmento.id = "anillo"+i; /* ... SU IMAGEN DE FONDO, ... */ elSegmento.style.background = "url(https://e1140ac37cfe956d7b5657ebb2a7d4e001ade301.googledrive.com/host/0Bwv59jgRLbvUelpIdnpCY2F5WXc/segEscolopendra"+i+".png) 0 0 no-repeat"; /* SU ALTURA, ... */ elSegmento.style.height = altos[i]+"px"; /* SU POSICIÓN, ... */ elSegmento.style.top = topes[i]+"px"; /* ... Y LE ASIGNA UN EVENTO transitionend QUE DISPARA UNA FUNCIÓN */ elSegmento.addEventListener("transitionend", mueve, false); } /* REFERENCIA AL ÚLTIMO span */ var ultimoSegmento = document.querySelectorAll("#cajaEntomologica span")[altos.length-1]; /* LE ASIGNA UN EVENTO click QUE TAMBIÉN DISPARA LA FUNCIÓN */ ultimoSegmento.addEventListener("click", mueve, false); /* Y UN ESTILO DE PUNTERO MANECILLA */ ultimoSegmento.style.cursor = "pointer"; } /* CAMBIA ÁNGULOS */ function mueve(e) { //console.log(e+" "+e.target.id) //e.stopPropagation(); /* SI NO ES EL PRIMER ELEMENTO DE LOS ANIDADOS ... */ if(e.target.parentElement.id != "anillo0") { /* CAMBIA EL ÁNGULO AL AZAR DENTRO DE UN RANGO DE +/- 20deg */ e.target.parentElement.style.transform = "rotate("+ Math.floor((Math.random()*40)-20) +"deg)"; } } onload = inicia; </script>

Y por último detallo sólo las diferencias:

  • La asignación de eventos escuchados ya está más limpia
    • elSegmento.addEventListener("transitionend", mueve, false);
      ultimoSegmento.addEventListener("click", mueve, false);
  • Pero la función aún tiene un argumento que parece no recoger nada.
    Resulta que este método tiene un argumento que pasa primero aunque no lo declaremos: es el mismo evento que dispara la función, y que sí puede ser capturada por ésta como argumento de event. Por eso está escrita como function mueve(e), esa e es el evento.
  • Pero a nosotros el evento por sí mismo no nos sirve, porque necesitamos una referencia al elemento donde se dispara para encontrar a su padre y cambiarle el estilo. Entonces lo llamamos igual, usando la sintaxis e.target , que sería "el elemento adonde apunta el evento". Sí, justo el elemento que queremos.
  • Hay también por ahí una línea para la consola, y otra más que me parece nunca mencionamos. Ya sabemos que los eventos se propagan, y en estos proyectos usamos esa característica para dar un movimiento que parezca aún más aleatorio (si eso es posible). Pero javascript tiene una forma de cortar la propagación en burbuja, y es aplicar al evento un stopPropagation.
    Si se mira en la consola, el cambio es notable, porque los eventos ahora se disparan una sola vez por vuelta.

Nos hacemos ecologistas, y dejamos de mortificar animales

Para terminar veamos un efecto totalmente distinto. Cada vez que disparamos una animación de estilo en un elemento, cambia —en secuencia— otro estilo del mismo elemento, pero de los que no son animables por CSS.

See the Pen Galería manual, vintage. by solipsistaCP (@solipsistacp) on CodePen.

(Imagen de The British Library.)

Para probarlo, hay que poner el puntero sobre el visor de la cámara hasta que se oscurezca, y al retirarlo la imagen habrá cambiado. El "disparo del obturador" tiene unos 0.2 segundos, si se demora menos en pasar el puntero, es casi seguro que la secuencia se va a alterar; pero al margen de eso, no es un mal efecto.

Aquí está el secreto

<script type="text/javascript"> /* LISTA DE IMÁGENES */ var ruta = ["https://e1140ac37cfe956d7b5657ebb2a7d4e001ade301.googledrive.com/host/0Bwv59jgRLbvUelpIdnpCY2F5WXc/aveRamaAnim1.gif", "https://e1140ac37cfe956d7b5657ebb2a7d4e001ade301.googledrive.com/host/0Bwv59jgRLbvUelpIdnpCY2F5WXc/aveRamaAnim2.gif", "https://e1140ac37cfe956d7b5657ebb2a7d4e001ade301.googledrive.com/host/0Bwv59jgRLbvUelpIdnpCY2F5WXc/aveRamaAnim3.gif"]; var cambia = 1;//REFERENCIA PARA ORDEN DE IMÁGENES var caja; function inicia() { /* REFERENCIA CONTENEDOR DE IMÁGENES DE FONDO */ caja = document.querySelector("div"); /* PONE COMO FONDO LA PRIMER IMAGEN */ caja.style.backgroundImage = "url("+ruta[0]+")"; /* AGREGA EL EVENTO transitionend QUE EJECUTA UNA FUNCIÓN */ caja.addEventListener("transitionend", pia, false); } /* CAMBIA LAS IMÁGENES */ function pia() { //console.log("ruta["+cambia+"] ACTUAL"); /* SI LA REFERENCIA AL ELEMENTO DE ARRAY ES VÁLIDA */ if(ruta[cambia]) { /* REEMPLAZA LA IMAGEN SEGÚN EL VALOR DE cambia */ caja.style.backgroundImage = "url("+ ruta[cambia] +")"; } /* MODIFICA EL VALOR DE 'cambia' DE FORMA CIRCULAR */ cambia = (cambia == ruta.length - .5)? 0 : cambia + .5; //console.log("ruta["+cambia+"] PRÓXIMO"); } onload = inicia; </script>

Es demasiado simple para ser real; pero seguro más de uno se quedó pensando en ese incremento de 0.5 . Es otro viejo truco para hacer cambios vuelta por medio.

  • Empezamos con un array para las rutas de las imágenes que vamos a mostrar en secuencia o rotación.
  • Una variable global incremental nos va a servir para ir llamando uno a uno a los elementos de aquel array. Y otra es la referencia para la caja contenedora a la que aplicamos los estilos.
  • La función de inicio solamente pone la primera imagen como fondo de un div (el visor de la cámara), y luego le agrega el escuchador de eventos para llamar a la segunda función.
  • Ésta es muy sencilla, si le entendemos la lógica. Tenemos un condicional que comprueba si el elemento de array con el número de cambia es válido. Como empieza con '1' (el de la segunda imagen) resulta que ruta[1] existe, por lo que se cambia el fondo al poner el puntero encima cuando termina la animación de sombra que oscurece todo el div. Inmediatamente después hay otro condicional, que en el caso de que la variable sea igual al total de elementos (3) menos 0.5 (en total, 2.5) le asigna el valor '0'. En nuestro caso no es así, porque era '1', entonces le suma 0.5; y queda como '1.5'.
  • Al retirar el puntero se repite el evento, pero esta vez por aclarar el div cuando se reduce la sombra. Entonces se dispara la función nuevamente, y otra vez comprueba que el elemento de array exista. Pero resulta que ahora es ruta(1.5), que no puede existir de ninguna manera (los arrays aceptan como índice sólo números enteros y positivos), así que no cambia nada, y sigue con la imagen previa. En resumen, al oscurecer el visor se cambia la imagen mientras no la vemos, y al retirarse la sombra no la cambia de nuevo porque el valor que le pasamos al escript no es válido, y entonces seguimos viendo la imagen recién cambiada.
    Pero el incremento de la variable si ocurre, y al '1.5' le va a sumar 0.5; por lo que para la próxima ya vale '2'.
  • Al pasar otra vez el puntero la mecánica se repite. El visor se oscurece y se comprueba que la ahora ruta[2] exista. Y sí, existe, así que mientras está oscuro, el escript la cambia, y sube la variable a '2.5'. Al retirar el puntero ya la podemos ver, porque '2.5' no es un índice de array válido, y no se cambia el fondo de nuevo.
  • Ahora llegamos a la última de la serie. Como recordarán, hay un condicional que comprueba si cambia es igual al total de aves menos 0.5 (para nuestro ejemplo, 3 - 0.5 = 2.5). Como al mostrar la última imagen, ya vemos que el número de array queda justamente en '2.5', con el condicional que asigna a cambia el valor '0' nos aseguramos que recomience la serie con ruta[0] siempre después de mostrar la última.

Más posibilidades con 'animation'

Ya vimos que transition tiene un evento que se ejecuta cuando termina de moverse, pero usando keyframes tenemos más eventos para probar, ya que el navegador detecta cuando la animación empieza (animationstart), cuando itera (animationiteration) y por supuesto cuando termina (animationend). Seguro en algún momento vamos a probar estos eventos y sus posibilidades.

Autoría

Furoya: Autor del artículo

Artículo original de Furoya.
La intención del autor con sus colaboraciones no es que los artículos sirvan para hacer un copy&paste de códigos sino que comprendas y aprendas la lógica y el cómo trabaja javaScript.
Y a partir de lo expuesto experimentes tú.
El autor del post y el editor del blog te animamos a que plantees tus dudas o reflexiones y que compartas tus realizaciones en base a lo expuesto en los comentarios. Recuerda que puedes incluir pens (ejemplos en Codepen.io) en ellos.