Crear animaciones con JavaScript y AnimeJs

Aprende a crear animaciones de todo tipo utilizando AnimeJs, una librería JavaScript ideal para dar vida a tus interfaces gráficas.

https://animejs.com/ “The leading HTML5 client solution for generating PDFs. Perfect for event tickets, reports, certificates, you name it!”“A lightweight JavaScript animation library with a simple, yet powerful API. It works with CSS properties, SVG, DOM attributes and JavaScript Objects.” 

En la parte del final de éste artículo encontrarás un ejercicio práctico, donde se implementa la librería. Haz click en la siguiente imagen para ver el resultado final en una ventana nueva:

Ejercicio con AnimeJs
Haz click sobre la imagen para abrir el ejercicio en una ventana nueva. Crear animaciones con JavaScript y AnimeJs

¿Quieres aprender a hacer este ejercicio? Saltar directamente al tutorial

Hablar del diseño de animaciones en interfaces gráficas, daría para crear un blog entero dedicado a eso. Hoy, sin embargo, nos centraremos en explicar los conceptos más relevantes de este tema.Y por supuesto, veremos cómo implementarlos correctamente en nuestros proyectos. Al final de este artículo sabrás cómo crear animaciones geniales con JavaScript y la librería AnimeJS.

Las animaciones como sistema para guiar al usuario

Para conseguir un buen diseño de animación, que contribuya a mejorar la experiencia de usuario, es clave entender qué propiedades describen el movimiento. Y más importante aún, qué criterio se debería seguir a la hora de definir esas propiedades.

Guiar al usuario en su interacción debería ser, en todo momento, nuestro objetivo principal. Por eso, el diseño de la animación, tiene que estar al servicio de los principios de información, focalización y expresión.

Estas son definiciones conceptuales, ¿pero qué es exactamente animar?. En esencia, animar, consiste en interpolar valores en un intervalo de tiempo. Los valores pueden representar cualquier cosa. Desde una posición en un espacio 2D o 3D, hasta la opacidad de un elemento en pantalla.

Por otro lado, la interpolación, es quizá el componente más importante a tener en cuenta. La forma en la que se transiciona de un valor a otro, es la que le da expresividad al movimiento. Y es muy importante ser consciente de ello. De hecho, transicionar valores de forma lineal, es un error bastante común, cuando se empieza a animar.

En la naturaleza, todo movimiento requiere de una aceleración y de un tiempo de frenado. Por ese motivo, si un elemento de una interfaz gráfica, pasa de estar estático a moverse sin progresión ninguna, se percibe como algo artificial.

Crea transiciones que mejoren la experiencia de tus usuarios

AnimeJs, es una librería JavaScript que facilita la implementación de casi cualquier clase de animación. Su API ofrece un conjunto de herramientas para interpolar todo tipo de valores que, en última instancia, darán vida a nuestras interfaces.

Esta librería fue creada por Julian Garnier. A pesar de no estar activamente mantenida, recibe más de 140k descargas semanales en npmjs, y tiene más de 41k estrellas en Github.

Está disponible bajo el comando «npm install animejs«, y tras compilar, ocupa unos 17kb.

Utilizar su API es sorprendentemente sencillo. Basta con llamar directamente a la librería, pasando un objeto con opciones de configuración, en forma de clave / valor.

Del conjunto de opciones, la más relevante es «targets«. Este hace referencia a los elementos sobre los que se quiere aplicar una animación.

Targets admite múltiples variables. Desde selectores CSS, hasta objetos js. Incluso arrays, para combinar varios tipos distintos.

Otras opciones de configuración, sirven para determinar sobre qué propiedades de los elementos se aplica la transición. AnimeJs es capaz de interpolar cualquier propiedad que se pueda expresar numéricamente. Por poner algunos ejemplos, admite propiedades de CSS (transformaciones incluidas). Atributos de elementos SVG y del DOM. Y propiedades de objetos JavaScript.

El resto de opciones describen la animación. Permitiendo controlar características como la duración, la dirección, el tipo de transición o el «delay«.

Por último, cabe mencionar que, a través del método «timeline«, AnimeJs permite encadenar múltiples animaciones. Timeline es ideal para diseñar una secuencia armónica, de entradas y salidas de elementos en pantalla.

Aprende a animar tu interfaz web con AnimeJs

A continuación, realizaremos un pequeño ejercicio que nos permitirá entender cómo se implementa AnimeJS.

Dicho ejercicio consistirá en mostrar un listado de contactos. Al hacer “click” sobre uno de estos, se transicionará a un apartado detalle con más información. Por supuesto, la transición se realizará con la API de AnimeJS.

Con el objetivo de ilustrar la importancia de las animaciones, también añadiremos un botón para activar y desactivar la transición. De este modo, el usuario podrá comprobar la diferencia por sí mismo.

La primera parte, como suele ser habitual, es preparar la estructura HTML. Empezaremos por crear un elemento “div”, con la clase “page-wrapper”. Este elemento, actuará de contenedor principal, dentro del cual, añadiremos 4 “divs” más. Cada una de estas “divs” actuará de contenedor para cada contacto. 

El contenido de estas “tarjetas” de contacto, lo separaremos en los apartados “collapsedContent” y “expandedContent”. El primero, mostrará parte de la información antes de la transición. El segundo, contendrá la información que queremos mostrar al ser clicado.

    <div class="page-wrapper">
      <div class="expandable-card">
        <div cardContent>
          <div collapsedContent>
            <div class="expandable-card--left-col col">
              <div class="bg-card">
                <img src="imgs/1.jpeg" alt="">
              </div>
              <div class="expandable-card--profile-image">
                <span>JD</span>
                <img
                  src="https://randomuser.me/api/portraits/women/54.jpg"
                  alt=""
                />
              </div>
            </div>
            <div class="expandable-card--right-col col">
              <div class="expandable-card--title">
                Jane Doe
              </div>
            </div>
          </div>
          <div expandedContent>
            <div class="profile-data">
              <h1>Jane Doe</h1>
              <h2>03/03/1976</h2>
              <p>St. Louis, MO</p>
            </div>
          </div>
        </div>
      </div>
      <div class="expandable-card">
        <div cardContent>
          <div collapsedContent>
            <div class="expandable-card--left-col col">
              <div class="bg-card">
                <img src="imgs/2.jpeg" alt="">
              </div>
              <div class="expandable-card--profile-image profile-blue">
                <span>MB</span>
                <img
                  src="https://randomuser.me/api/portraits/men/54.jpg"
                  alt=""
                />
              </div>
            </div>
            <div class="expandable-card--right-col col">
              <div class="expandable-card--title">
                Mark Bale
              </div>
            </div>
          </div>
          <div expandedContent>
            <div class="profile-data">
              <h1>Mark Bale</h1>
              <h2>06/11/1984</h2>
              <p>St. Charles, CN</p>
            </div>
          </div>
        </div>
      </div>
      <div class="expandable-card">
        <div cardContent>
          <div collapsedContent>
            <div class="expandable-card--left-col col">
              <div class="bg-card">
                <img src="imgs/3.jpeg" alt="">
              </div>
              <div class="expandable-card--profile-image profile-pink">
                <span>RV</span>
                <img
                  src="https://randomuser.me/api/portraits/women/15.jpg"
                  alt=""
                />
              </div>
            </div>
            <div class="expandable-card--right-col col">
              <div class="expandable-card--title">
                Rose Vila
              </div>
            </div>
          </div>
          <div expandedContent>
            <div class="profile-data">
              <h1>Rose Vila</h1>
              <h2>12/02/1996</h2>
              <p>St. Roye, 87</p>
            </div>
          </div>
        </div>
      </div>
      <div class="expandable-card">
        <div cardContent>
          <div collapsedContent>
            <div class="expandable-card--left-col col">
              <div class="bg-card">
                <img src="imgs/4.jpeg" alt="">
              </div>
              <div class="expandable-card--profile-image profile-orange">
                <span>DS</span>
                <img
                  src="https://randomuser.me/api/portraits/women/76.jpg"
                  alt=""
                />
              </div>
            </div>
            <div class="expandable-card--right-col col">
              <div class="expandable-card--title">
                Dan Smith
              </div>
            </div>
          </div>
          <div expandedContent>
            <div class="profile-data">
              <h1>Dan Smith</h1>
              <h2>18/10/1984</h2>
              <p>St. West, 17</p>
            </div>
          </div>
        </div>
      </div>
      <div class="toggle-animation-wrapper">
        <label for="animation-switch">Activar / Desactivar animaciones</label>
        <input type="checkbox" id="animation-switch" class="animation-switch" />
      </div>
    </div>

Justo antes de cerrar el contenedor “page-wrapper”, también añadiremos un input de tipo “checkbox” con su “label”.

El siguiente paso, será añadir estilos a las tarjetas. Para ello utilizaremos SCSS. Definiremos los siguientes estilos para posicionar los elementos dentro de las tarjetas. E incluso, ocultaremos aquellos que sólo deben aparecer cuando la tarjeta esté expandida.

@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;700&display=swap");
$color-primary: #69f0ae;
$color-secondary: #18ffff;
$color-bg: #37474f;
* {
  font-family: "Open Sans", sans-serif;
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body,
html {
  background: $color-bg;
}
.page-wrapper {
  max-width: 860px;
  padding: 40px;
  margin: 0 auto;
  .expandable-card {
    position: relative;
    margin-bottom: 16px;
    .expandable-card--title {
      font-size: 21px;
      font-weight: bold;
      line-height: 24px;
      margin-bottom: 8px;
      margin-top: 15px;
    }
    .col:first-child {
      margin-right: 24px;
    }
    .expandable-card--profile-image {
      width: 64px;
      height: 64px;
      display: flex;
      align-items: center;
      justify-content: center;
      background: $color-primary;
      color: black;
      font-size: 21px;
      font-weight: bold;
      overflow: hidden;
      border-radius: 1000px;
      border: solid 3px white;
      position: relative;
      &.profile-blue {
        background-color: aqua;
      }
      &.profile-pink {
        background-color: rgb(255, 109, 199);
      }
      &.profile-orange {
        background-color: rgb(255, 180, 82);
      }
      img {
        opacity: 0;
        position: absolute;
      }
    }
    [cardContent] {
      position: relative;
      background: white;
      border-radius: 8px;
      overflow: hidden;
      box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.1);
      cursor: pointer;
      [collapsedContent] {
        display: flex;
        flex-direction: row;
        padding: 24px;
        .bg-card {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 120px;
          opacity: 0;
          img {
            width: 100%;
            height: 100%;
            display: block;
            object-fit: cover;
            object-position: center center;
          }
        }
      }
      [expandedContent] {
        visibility: hidden;
        position: absolute;
        left: 0;
        right: 0;
        opacity: 0;
        padding: 24px;
        overflow: hidden;
        color: #222222;
        .profile-data {
          padding-top: 20px;
          text-align: center;
        }
      }
    }
  }
  .toggle-animation-wrapper {
    background-color: #212f36;
    padding: 5px;
    border-radius: 3px;
    text-align: center;
    label {
      color: white;
      cursor: pointer;
    }
  }
}
.expanding-card--backdrop {
  background: black;
  opacity: 0.5;
}

Finalmente programaremos el código JavaScript, aprovechando las funcionalidades que nos ofrece AnimeJs. Para una mayor comodidad, separaremos el programa en dos archivos, index.js y ExpandableCard.js.

El primero, empezará importando la clase “ExpandableCard” y la función “toggleAnimation” del otro archivo. Seguidamente, también se importarán los estilos.

Tras importar estos recursos, index.js se encargará, por un lado, de escuchar el evento “change” del checkbox, y llamar a “toggleAnimation”. Y por el otro, de recorrer todos los elementos del DOM con la clase “.expandable-card”, e instanciar “ExpandableCard” para cada uno.

import ExpandableCard, { toggleAnimation } from "./ExpandableCard";
import "./SCSS/index.scss";
document
  .querySelector(".animation-switch")
  .addEventListener("change", toggleAnimation);
document.querySelectorAll(".expandable-card").forEach((card) => {
  new ExpandableCard(card);
});

En el archivo ExpandableCard.js empezaremos importando la librería animejs. 

import anime from "animejs";

Seguidamente declaramos dos variables para controlar los tiempos de transición. Estas variables serán “duration” y “leaveDuration”.

let duration = 300;
let leaveDuration = 250;

Justo después, declararemos y exportamos la función “​​toggleAnimation”. Esta función actuará de activador y desactivador de las animaciones. Para ello, modificará el valor de las variables de tiempo de 0 a su valor inicial, y viceversa, cada vez que se llame.

export const toggleAnimation = () => {
  duration = duration === 0 ? 300 : 0;
  leaveDuration = leaveDuration === 0 ? 250 : 0;
};

El siguiente elemento a crear es la clase ExpandableCard. Esta es una clase, y requiere de ser instanciada pasándole un elemento del DOM con la estructura descrita anteriormente. En el constructor se guardarán referencias al propio nodo pasado como argumento, y a elementos hijos de este.

También crearemos elementos nuevos como “backdropEl”, y “placeholderEl”. El primero actuará como capa de fondo, que aparecerá detrás de la tarjeta al expandirse. El segundo, será un bloque transparente añadido a la lista de contactos. El objetivo de este bloque es mantener el espacio entre tarjetas que queda al expandir una de ellas.

Aún dentro del constructor, declaremos propiedades de control como “expanded”, “animatingFlag”, “initialHeight” y “windowResized”. Por último, escuchamos los eventos de click sobre los elementos “backdropEl”, y “hostEl”, para llamar a los métodos “collapse” y “expand”.

class ExpandableCard {
  constructor(node) {
    this.backdropEl = document.createElement("div");
    this.backdropEl.className = "expanding-card--backdrop";
    this.hostEl = node;
    this.placeholderEl = document.createElement("div");
    this.placeholderEl.className = "expanding-card--placeholder";
    this.hostEl.appendChild(this.placeholderEl);
    this.cardContentEl = node.querySelector("[cardContent]");
    this.collapsedContentEl = node.querySelector("[collapsedContent]");
    this.expandedContentEl = node.querySelector("[expandedContent]");
    this.initialHeight = 0;
    this.windowResized = false;
    this.expanded = false;
    this.animatingFlag = false;
    this.backdropEl.addEventListener("click", () => {
      this.collapse();
    });
    this.hostEl.addEventListener("click", () => {
      this.expand();
    });
  }
}

Antes de explicar qué pasa dentro de “collapse” y “expand”, declaramos un par de métodos de ayuda.

handleWindowResize”, solo se encargará de setear la propiedad windowResized a true.

 handleWindowResize() {
    this.windowResized = true;
 }

staticHeight”, recibirá un elemento del DOM, al que le seteará su altura en los estilos en línea, para, posteriormente, devolver esa altura.

staticHeight(node) {
    const height = node.offsetHeight;
    node.style.height = `${height}px`;
    return height;
  }

Ahora si, ya lo tenemos todo listo para definir “expand” y “collapse”.

El método “expand”, empezará comprobando las propiedades “expanded” y “animating”, para determinar si sigue con la función.

Antes de empezar a animar, definiremos los estilos de inicio de los elementos guardados como propiedades del objeto.

También definiremos las variables “fromHeight”, “toHeight”, “cardBoundingRect” y “targetBoundingRect”. Estas variables guardarán el tamaño y posición inicial y final de la tarjeta.

expand() {
    if (this.expanded || this.animating) {
      return;
    }
    this.animating = true;
    this.backdropEl.style.position = "fixed";
    this.backdropEl.style.top = "0px";
    this.backdropEl.style.left = "0px";
    this.backdropEl.style.right = "0px";
    this.backdropEl.style.bottom = "0px";
    this.backdropEl.style.opacity = "0";
    this.backdropEl.style.zIndex = 9;
    document.body.appendChild(this.backdropEl);
    const cardBoundingRect = this.cardContentEl.getBoundingClientRect();
    this.placeholderEl.style.height = `${cardBoundingRect.height}px`;
    this.cardContentEl.style.position = "fixed";
    this.cardContentEl.style.zIndex = 10;
    this.cardContentEl.style.top = `${0}px`;
    this.cardContentEl.style.left = `${0}px`;
    this.cardContentEl.style.width = null;
    this.cardContentEl.style.height = null;
    this.cardContentEl.style.transform = `translate(${cardBoundingRect.left}px), ${cardBoundingRect.top}px)`;
    this.expandedContentEl.style.visibility = "visible";
    const fromHeight = this.staticHeight(this.cardContentEl);
    this.initialHeight = fromHeight;
    const toHeight = 300;
    const targetBoundingRect = {
      left: 16,
      top: 16,
      width: window.innerWidth - 32,
      height: toHeight,
    };
  }

Una vez preparados los estados iniciales de los elementos, podemos crear las animaciones con AnimeJs. Las animaciones se declaran llamando directamente a la librería “anime”. Al llamar a esta función debemos pasar un objeto con distintas propiedades. La propiedad “targets” hará referencia al elemento sobre el que aplicaremos las animaciones. El resto de propiedades servirán para definir qué estilos animar, qué duración, y el tipo de “easing”.

Guardaremos las “promises” devueltas por cada animación en una array. Y ejecutaremos una instrucción para actualizar “animating” y “expanded” al resolverlas todas.

    const promises = [
      anime({
        targets: this.cardContentEl.querySelector(".bg-card"),
        opacity: [0, 1],
        duration: duration,
      }).finished,
      anime({
        targets: this.cardContentEl.querySelector(
          ".expandable-card--profile-image"
        ),
        translateX: [0, 0],
        duration: duration,
        scale: [1, 3],
      }).finished,
      anime({
        targets: this.cardContentEl.querySelector(
          ".expandable-card--profile-image img"
        ),
        opacity: [0, 1],
        scale: [1, 0.5],
        duration: duration,
        easing: "easeOutCubic",
      }).finished,
      anime({
        targets: this.cardContentEl.querySelector(
          ".expandable-card--right-col"
        ),
        opacity: [1, 0],
        duration: duration,
        easing: "easeOutCubic",
      }).finished,
      anime({
        targets: this.cardContentEl,
        height: [fromHeight, toHeight],
        width: [cardBoundingRect.width, targetBoundingRect.width],
        translateX: [cardBoundingRect.left, targetBoundingRect.left],
        translateY: [cardBoundingRect.top, targetBoundingRect.top],
        boxShadow:
          "0 0 1px 0 rgba(33,43,54,.08), 0 8px 10px 0 rgba(33,43,54,.2)",
        duration: duration,
        easing: "easeOutCubic",
      }).finished,
      anime({
        targets: this.expandedContentEl,
        translateY: [16, 0],
        opacity: [0, 1],
        delay: 200,
        duration: duration,
        easing: "easeOutCubic",
      }).finished,
      anime({
        targets: this.backdropEl,
        opacity: [0, 0.33],
        duration: duration,
        easing: "easeOutCubic",
      }),
    ];
    return Promise.all(promises).then(() => {
      window.addEventListener("resize", this.handleWindowResize);
      this.animating = false;
      this.expanded = true;
    });

El método “collapse” va a estructurarse de forma muy parecida a “expand”. Tras la comprobación inicial, preparamos el estado de los elementos del DOM.

  collapse() {
    if (!this.expanded || this.animating) {
      return;
    }
    this.animating = true;
    const placeholderRect = this.placeholderEl.getBoundingClientRect();
    const cardContentRect = this.cardContentEl.getBoundingClientRect();
    const expandedContentHeight = this.expandedContentEl.offsetHeight;
    const fromHeight = 300;
    const toHeight = !this.windowResized ? this.initialHeight : fromHeight - expandedContentHeight;
  }

Generamos el array de “promises”, con todas las animaciones que se aplicarán. Es importante, mantener la coherencia entre los valores de animación finales de “expand”, y los de inicio de “collapse”. De no hacerlo así, provocaremos saltos indeseados.

Al igual que en el caso de “expand”, tras resolver todas las promises, lanzaremos la siguiente instrucción. 

En ella se restablecen los estilos iniciales, para cerrar el ciclo de animación “expand”/”collapse”.

const promises = [
      anime({
        targets: this.cardContentEl.querySelector(".bg-card"),
        opacity: [1, 0],
        duration: leaveDuration,
        easing: "easeInQuad",
      }).finished,
      anime({
        targets: this.cardContentEl.querySelector(
          ".expandable-card--profile-image"
        ),
        translateX: [0, 0],
        duration: leaveDuration,
        scale: [3, 1],
        easing: "easeInQuad",
      }).finished,
      anime({
        targets: this.cardContentEl.querySelector(
          ".expandable-card--profile-image img"
        ),
        opacity: [1, 0],
        scale: [0.5, 1],
        duration: leaveDuration,
        easing: "easeInQuad",
      }).finished,
      anime({
        targets: this.cardContentEl.querySelector(
          ".expandable-card--right-col"
        ),
        opacity: [0, 1],
        duration: leaveDuration,
        easing: "easeInQuad",
      }).finished,
      anime({
        targets: this.cardContentEl,
        height: [fromHeight, toHeight],
        translateX: [cardContentRect.left, placeholderRect.left],
        translateY: [cardContentRect.top, placeholderRect.top],
        width: [cardContentRect.width, placeholderRect.width],
        boxShadow: "0 2px 2px 1px rgba(0, 0, 0, 0.1)",
        duration: leaveDuration,
        delay: 0,
        easing: "easeInQuad",
      }).finished,
      anime({
        targets: this.expandedContentEl,
        translateY: [0, 16],
        opacity: [1, 0],
        duration: leaveDuration,
        easing: "easeInQuad",
      }).finished,
      anime({
        targets: this.backdropEl,
        opacity: [0.33, 0],
        duration: leaveDuration,
        easing: "easeInQuad",
      }),
    ];
    return Promise.all(promises).then(() => {
      this.animating = false;
      this.expanded = false;
      this.placeholderEl.style.height = "0px";
      this.cardContentEl.style.position = "relative";
      this.cardContentEl.style.zIndex = null;
      this.cardContentEl.style.top = null;
      this.cardContentEl.style.left = null;
      this.cardContentEl.style.width = null;
      this.cardContentEl.style.height = null;
      this.cardContentEl.style.transform = null;
      this.expandedContentEl.style.visibility = "hidden";
      document.body.removeChild(this.backdropEl);
      this.windowResized = false;
      window.removeEventListener("resize", this.handleWindowResize);
    });

Crear animaciones con JavaScript y AnimeJs

Este ha sido un pequeño ejercicio para ilustrar el potencial de AnimeJs. Tal y como hemos visto, crear animaciones con JavaScript y AnimeJs es pan comido.

Adicionalmente, la animación también es una potente herramienta para el «historytelling«. Buenos ejemplos de ello, son las páginas donde el usuario hace scroll, y la página responde encadenando infografías animadas. Creando un paralelismo entre la barra de scroll, y el “timeline” de un audiovisual.

Otro campo donde las animaciones juegan un papel fundamental es el de visualización de datos. Animar gráficos puede servir para enfatizar la información que estos representan. Como ya vimos en el repaso de la librería RoughViz, forma y contenido van de la mano.

Este ha sido un pequeño vistazo a la librería AnimeJs. A continuación os dejo enlaces a más recursos, así como el ejercicio subido al repositorio de Github de “Librerías Js”:

Nos vemos pronto, un abrazo desarrolladores!

Deja un comentario