Las animaciones de JavaScript pueden manejar cosas que CSS no puede.
Por ejemplo, moverse a lo largo de una ruta compleja, con una función de sincronización diferente a las curvas de Bézier, o una animación en un canvas.
Usando setInterval
Una animación se puede implementar como una secuencia de frames, generalmente pequeños cambios en las propiedades de HTML/CSS.
Por ejemplo, cambiar style.left
de 0px
a 100px
mueve el elemento. Y si lo aumentamos en setInterval
, cambiando en 2px
con un pequeño retraso, como 50 veces por segundo, entonces se ve suave. Ese es el mismo principio que en el cine: 24 frames por segundo son suficientes para que se vea suave.
El pseudocódigo puede verse así:
let timer = setInterval(function() {
if (animation complete) clearInterval(timer);
else increase style.left by 2px
}, 20); // cambiar en 2px cada 20ms, aproximadamente 50 frames por segundo
Ejemplo más completo de la animación:
let start = Date.now(); // recordar la hora de inicio
let timer = setInterval(function() {
// ¿Cuánto tiempo pasó desde el principio?
let timePassed = Date.now() - start;
if (timePassed >= 2000) {
clearInterval(timer); // terminar la animación después de 2 segundos
return;
}
// dibujar la animación en el momento timePassed
draw(timePassed);
}, 20);
// mientras timePassed va de 0 a 2000
// left obtiene valores de 0px a 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
Haz clic para ver la demostración:
<!DOCTYPE HTML>
<html>
<head>
<style>
#train {
position: relative;
cursor: pointer;
}
</style>
</head>
<body>
<img id="train" src="https://js.cx/clipart/train.gif">
<script>
train.onclick = function() {
let start = Date.now();
let timer = setInterval(function() {
let timePassed = Date.now() - start;
train.style.left = timePassed / 5 + 'px';
if (timePassed > 2000) clearInterval(timer);
}, 20);
}
</script>
</body>
</html>
Usando requestAnimationFrame
Imaginemos que tenemos varias animaciones ejecutándose simultáneamente.
Si las ejecutamos por separado, aunque cada una tenga setInterval (..., 20)
, el navegador tendría que volver a pintar con mucha más frecuencia que cada 20ms
.
Eso es porque tienen un tiempo de inicio diferente, por lo que “cada 20ms” difiere entre las diferentes animaciones. Los intervalos no están alineados. Así que tendremos varias ejecuciones independientes dentro de 20ms
.
En otras palabras, esto:
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
…Es más ligero que tres llamadas independientes:
setInterval(animate1, 20); // animaciones independientes
setInterval(animate2, 20); // en diferentes lugares del script
setInterval(animate3, 20);
Estos varios redibujos independientes deben agruparse para facilitar el redibujado al navegador y, por lo tanto, cargar menos CPU y verse más fluido.
Hay una cosa más a tener en cuenta. A veces, cuando el CPU está sobrecargado, o hay otras razones para volver a dibujar con menos frecuencia (como cuando la pestaña del navegador está oculta), no deberíamos ejecutarlo cada 20ms
.
Pero, ¿cómo sabemos eso en JavaScript? Hay una especificación Sincronización de animación que proporciona la función requestAnimationFrame
. Aborda todos estos problemas y aún más.
La sintaxis:
let requestId = requestAnimationFrame(callback)
Eso programa la función callback
para que se ejecute en el tiempo más cercano cuando el navegador quiera hacer una animación.
Si hacemos cambios en los elementos dentro de callback
, entonces se agruparán con otros callbacks de requestAnimationFrame
y con animaciones CSS. Así que habrá un recálculo y repintado de geometría en lugar de muchos.
El valor devuelto requestId
se puede utilizar para cancelar la llamada:
// cancelar la ejecución programada del callback
cancelAnimationFrame(requestId);
El callback
obtiene un argumento: el tiempo transcurrido desde el inicio de la carga de la página en microsegundos. Este tiempo también se puede obtener llamando a performance.now().
Por lo general, el callback
se ejecuta muy pronto, a menos que el CPU esté sobrecargado o la batería de la laptop esté casi descargada, o haya otra razón.
El siguiente código muestra el tiempo entre las primeras 10 ejecuciones de requestAnimationFrame
. Por lo general, son 10-20ms:
<script>
let prev = performance.now();
let times = 0;
requestAnimationFrame(function measure(time) {
document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
prev = time;
if (times++ < 10) requestAnimationFrame(measure);
})
</script>
Animación estructurada
Ahora podemos hacer una función de animación más universal basada en requestAnimationFrame
:
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction va de 0 a 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// calcular el estado actual de la animación
let progress = timing(timeFraction)
draw(progress); // dibujar
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
La función animate
acepta 3 parámetros que básicamente describen la animación:
duration
-
Tiempo total de animación. Como:
1000
. timing(timeFraction)
-
Función de sincronización, como la propiedad CSS
transition-timing-function
que obtiene la fracción de tiempo que pasó (0
al inicio,1
al final) y devuelve la finalización de la animación (comoy
en la curva de Bézier).Por ejemplo, una función lineal significa que la animación continúa uniformemente con la misma velocidad:
function linear(timeFraction) { return timeFraction; }
Su gráfico:
Eso es como
transition-timing-function: linear
. A continuación se muestran variantes más interesantes. draw(progress)
-
La función que toma el estado de finalización de la animación y la dibuja. El valor
progress=0
denota el estado inicial de la animación yprogress=1
– el estado final.Esta es la función que realmente dibuja la animación.
Puede mover el elemento:
function draw(progress) { train.style.left = progress + 'px'; }
…O hacer cualquier otra cosa, podemos animar cualquier cosa, de cualquier forma.
Vamos a animar el elemento width
de 0
a 100%
usando nuestra función.
Haz clic en el elemento de la demostración:
function animate({duration, draw, timing}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
let progress = timing(timeFraction)
draw(progress);
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
progress {
width: 5%;
}
</style>
<script src="animate.js"></script>
</head>
<body>
<progress id="elem"></progress>
<script>
elem.onclick = function() {
animate({
duration: 1000,
timing: function(timeFraction) {
return timeFraction;
},
draw: function(progress) {
elem.style.width = progress * 100 + '%';
}
});
};
</script>
</body>
</html>
El código para ello:
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
A diferencia de la animación CSS, aquí podemos hacer cualquier función de sincronización y cualquier función de dibujo. La función de sincronización no está limitada por las curvas de Bézier. Y draw
puede ir más allá de las propiedades, crear nuevos elementos para la animación de fuegos artificiales o algo así.
Funciones de sincronización
Vimos arriba la función de sincronización lineal más simple.
Veamos más de ellas. Intentaremos animaciones de movimiento con diferentes funciones de sincronización para ver cómo funcionan.
Potencia de n
Si queremos acelerar la animación, podemos usar progress
en la potencia n
.
Por ejemplo, una curva parabólica:
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
La gráfica:
Velo en acción (haz clic para activar):
…O la curva cúbica o incluso mayor n
. Aumentar la potencia hace que se acelere más rápido.
Aquí está el gráfico de progress
en la potencia 5
:
En acción:
El arco
Función:
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
La gráfica:
Back: tiro con arco
Esta función realiza el “tiro con arco”. Primero “tiramos de la cuerda del arco”, y luego “disparamos”.
A diferencia de las funciones anteriores, depende de un parámetro adicional x
, el “coeficiente de elasticidad”. La distancia de “tirar de la cuerda del arco” está definida por él.
El código:
function back(x, timeFraction) {
return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x)
}
The graph for x = 1.5
:
Para la animación lo usamos con un valor específico de x
. Ejemplo de x = 1.5
:
Rebotar
Imagina que dejamos caer una pelota. Se cae, luego rebota unas cuantas veces y se detiene.
La función bounce
hace lo mismo, pero en orden inverso: el “rebote” comienza inmediatamente. Utiliza algunos coeficientes especiales para eso:
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
En acción:
Animación elástica
Una función “elástica” más que acepta un parámetro adicional x
para el “rango inicial”.
function elastic(x, timeFraction) {
return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}
La gráfica para x=1.5
:
En acción para x=1.5
:
Inversión: ease*
Entonces tenemos una colección de funciones de sincronización. Su aplicación directa se llama “easyIn”.
A veces necesitamos mostrar la animación en orden inverso. Eso se hace con la transformación “easyOut”.
easeOut
En el modo “easyOut”, la función de sincronización
se coloca en un wrapper timingEaseOut
:
timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)
En otras palabras, tenemos una función de “transformación” makeEaseOut
que toma una función de sincronización “regular” y devuelve el wrapper envolviéndola:
// acepta una función de sincronización, devuelve la variante transformada
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
Por ejemplo, podemos tomar la función bounce
descrita anteriormente y aplicarla:
let bounceEaseOut = makeEaseOut(bounce);
Entonces el rebote no estará al principio, sino al final de la animación. Se ve aún mejor:
#brick {
width: 40px;
height: 20px;
background: #EE6B47;
position: relative;
cursor: pointer;
}
#path {
outline: 1px solid #E8C48E;
width: 540px;
height: 20px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<div id="path">
<div id="brick"></div>
</div>
<script>
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
let bounceEaseOut = makeEaseOut(bounce);
brick.onclick = function() {
animate({
duration: 3000,
timing: bounceEaseOut,
draw: function(progress) {
brick.style.left = progress * 500 + 'px';
}
});
};
</script>
</body>
</html>
Aquí podemos ver cómo la transformación cambia el comportamiento de la función:
Si hay un efecto de animación al principio, como rebotar, se mostrará al final.
En el gráfico anterior, el rebote regular tiene el color rojo y el rebote easyOut es azul.
- Rebote regular: el objeto rebota en la parte inferior y luego, al final, salta bruscamente hacia la parte superior.
- Después de
easyOut
– primero salta a la parte superior, luego rebota allí.
easeInOut
También podemos mostrar el efecto tanto al principio como al final de la animación. La transformación se llama “easyInOut”.
Dada la función de tiempo, calculamos el estado de la animación de la siguiente manera:
if (timeFraction <= 0.5) { // primera mitad de la animación
return timing(2 * timeFraction) / 2;
} else { // segunda mitad de la animación
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
El código wrapper:
function makeEaseInOut(timing) {
return function(timeFraction) {
if (timeFraction < .5)
return timing(2 * timeFraction) / 2;
else
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
}
bounceEaseInOut = makeEaseInOut(bounce);
En acción, bounceEaseInOut
:
#brick {
width: 40px;
height: 20px;
background: #EE6B47;
position: relative;
cursor: pointer;
}
#path {
outline: 1px solid #E8C48E;
width: 540px;
height: 20px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<div id="path">
<div id="brick"></div>
</div>
<script>
function makeEaseInOut(timing) {
return function(timeFraction) {
if (timeFraction < .5)
return timing(2 * timeFraction) / 2;
else
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
let bounceEaseInOut = makeEaseInOut(bounce);
brick.onclick = function() {
animate({
duration: 3000,
timing: bounceEaseInOut,
draw: function(progress) {
brick.style.left = progress * 500 + 'px';
}
});
};
</script>
</body>
</html>
La transformación “easyInOut” une dos gráficos en uno: easyIn
(regular) para la primera mitad de la animación y easyOut
(invertido) – para la segunda parte.
El efecto se ve claramente si comparamos las gráficas de easyIn
, easyOut
y easyInOut
de la función de sincronización circ
:
- Rojo es la variante regular de
circ
(easeIn
). - Verde –
easeOut
. - Azul –
easeInOut
.
Como podemos ver, el gráfico de la primera mitad de la animación es el easyIn
reducido y la segunda mitad es el easyOut
reducido. Como resultado, la animación comienza y termina con el mismo efecto.
“Dibujar” más interesante
En lugar de mover el elemento podemos hacer otra cosa. Todo lo que necesitamos es escribir la función draw
adecuada.
Aquí está la escritura de texto animada “rebotando”:
textarea {
display: block;
border: 1px solid #BBB;
color: #444;
font-size: 110%;
}
button {
margin-top: 10px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<textarea id="textExample" rows="5" cols="60">Tomó su espada vorpal en mano:
Hace mucho tiempo que el enemigo manxome buscaba—
Así descansó junto al árbol Tumtum,
Y se quedó un rato en el pensamiento.
</textarea>
<button onclick="animateText(textExample)">¡Ejecuta la escritura animada!</button>
<script>
function animateText(textArea) {
let text = textArea.value;
let to = text.length,
from = 0;
animate({
duration: 5000,
timing: bounce,
draw: function(progress) {
let result = (to - from) * progress + from;
textArea.value = text.slice(0, Math.ceil(result))
}
});
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
</script>
</body>
</html>
Resumen
Para animaciones que CSS no puede manejar bien, o aquellas que necesitan un control estricto, JavaScript puede ayudar. Las animaciones de JavaScript deben implementarse a través de requestAnimationFrame
. Ese método integrado permite configurar una función callback para que se ejecute cuando el navegador esté preparando un repintado. Por lo general, es muy pronto, pero el tiempo exacto depende del navegador.
Cuando una página está en segundo plano, no se repinta en absoluto, por lo que el callback no se ejecutará: la animación se suspenderá y no consumirá recursos. Eso es genial.
Aquí está la función auxiliar animate
para configurar la mayoría de las animaciones:
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction va de 0 a 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// calcular el estado actual de la animación
let progress = timing(timeFraction);
draw(progress); // dibujar
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
Opciones:
duration
– el tiempo total de animación en ms.timing
– la función para calcular el progreso de la animación. Obtiene una fracción de tiempo de 0 a 1, devuelve el progreso de la animación, generalmente de 0 a 1.draw
– la función para dibujar la animación.
Seguramente podríamos mejorarlo, agregar más campanas y silbidos, pero las animaciones de JavaScript no se aplican a diario. Se utilizan para hacer algo interesante y no estándar. Por lo tanto, querrás agregar las funciones que necesitas cuando las necesites.
Las animaciones JavaScript pueden utilizar cualquier función de sincronización. Cubrimos muchos ejemplos y transformaciones para hacerlos aún más versátiles. A diferencia de CSS, aquí no estamos limitados a las curvas de Bézier.
Lo mismo ocurre con draw
: podemos animar cualquier cosa, no solo propiedades CSS.