Vi el video de Kevin Powell donde pudo recrear una bonita animación similar a una máquina de escribir usando CSS. Es genial y definitivamente deberías echarle un vistazo porque hay trucos CSS genuinos allí. Estoy seguro de que ha visto otros intentos de CSS en esto, incluido el fragmento de código de este sitio.
Como Kevin, decidí recrear la animación, pero la abrí a JavaScript. De esa manera, tenemos algunas herramientas adicionales que pueden hacer que la escritura se sienta un poco más natural e incluso más dinámica. Muchas de las soluciones CSS se basan en números mágicos basados en la longitud del texto, pero con JavaScript, podemos hacer algo que sea capaz de tomar cualquier texto que le arrojemos.
Entonces, hagámoslo. En este tutorial, mostraré que podemos animar varias palabras con solo cambiar el texto real. ¡No es necesario modificar el código cada vez que agrega una nueva palabra porque JavaScript lo hará por usted!
Empezando por el texto
Comencemos con el texto. Estamos usando una fuente monoespaciada para lograr el efecto. ¿Por qué? Porque cada carácter o letra ocupa la misma cantidad de espacio horizontal en una fuente monoespaciada, lo que será útil cuando usemos el concepto de steps()
mientras anima el texto. Las cosas son mucho más predecibles cuando ya conocemos el ancho exacto de un carácter y todos los caracteres comparten el mismo ancho.
Tenemos tres elementos colocados dentro de un contenedor: un elemento para el texto real, otro para ocultar el texto y otro para animar el cursor.
<div class="container">
<div class="text_hide"></div>
<div class="text">Typing Animation</div>
<div class="text_cursor"></div>
</div>
Podríamos usar ::before
y ::after
pseudo-elementos aquí, pero no son buenos para JavaScript. Los pseudoelementos no forman parte del DOM, sino que se utilizan como ganchos adicionales para diseñar un elemento en CSS. Sería mejor trabajar con elementos reales.
Ocultamos completamente el texto detrás del .text_hide
elemento. Esa es la clave. Es un div vacío que estira el ancho del texto y lo bloquea hasta que comienza la animación; ahí es cuando comenzamos a ver que el texto se mueve desde detrás del elemento.
Para cubrir todo el elemento de texto, coloque el .text_hide
elemento encima del elemento de texto que tiene la misma altura y ancho que el elemento de texto. Recuerde configurar el background-color
de El .text_hide
elemento exactamente igual que el del fondo que rodea el texto para que todo se mezcle.
.container {
position: relative;
}
.text {
font-family: 'Roboto Mono', monospace;
font-size: 2rem;
}
.text_hide {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
}
El cursor
A continuación, hagamos ese pequeño cursor que parpadea mientras se escribe el texto. Esperaremos la parte que parpadea por un momento y nos enfocaremos solo en el cursor.
Hagamos otro elemento con clase. .text_cursor
. Las propiedades van a ser similares a las .text_hide
elemento con una diferencia menor: en lugar de establecer un background-color
, mantendremos el background-color transparent
(ya que es técnicamente innecesario, agregue un borde al borde izquierdo del nuevo .text_cursor
elemento.
.text_cursor{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: transparent;
border-left: 3px solid black;
}
Ahora obtenemos algo que parece un cursor que está listo para moverse a medida que se mueve el texto:
Animación de JavaScript
Ahora viene la parte súper divertida: ¡animemos estas cosas con JavaScript! Comenzaremos envolviendo todo dentro de una función llamada typing_animation()
.
function typing_animation(){
// code here
}
typing_animation();
La siguiente tarea es almacenar todos y cada uno de los caracteres del texto en una sola matriz utilizando el split()
método. Esto divide la cadena en una subcadena que tiene solo un carácter y se devuelve una matriz que contiene todas las subcadenas.
function typing_animation(){
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
}
Por ejemplo, si tomamos “Typing Animation” como una cadena, entonces el resultado es:
También podemos determinar el número total de caracteres en la cadena. Para obtener solo las palabras en la cadena, reemplazamos split("")
con split(" ")
. Tenga en cuenta que hay una diferencia entre los dos. Aquí, " "
actúa como separador. Siempre que encontremos un solo espacio, terminará la subcadena y la almacenará como un elemento de matriz. Luego, el proceso continúa para toda la cadena.
function typing_animation(){
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
let all_words = text_element.innerHTML.split(" ");
}
Por ejemplo, para una cadena ‘Typing Animation’, la salida será,
Ahora, calculemos la longitud de toda la cadena, así como la longitud de todas y cada una de las palabras individuales.
function typing_animation() {
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
let all_words = text_element.innerHTML.split(" ");
let text_len = text_array.length;
const word_len = all_words.map((word) => {
return word.length;
});
}
Para obtener la longitud de la cadena completa, tenemos que acceder a la longitud de la matriz que contiene todos los caracteres como elementos individuales. Si estamos hablando de la longitud de una sola palabra, entonces podemos usar la map()
método, que accede una palabra a la vez desde el all_words
matriz y luego almacena la longitud de la palabra en una nueva matriz llamada word_len
. Ambas matrices tienen el mismo número de elementos, pero una contiene la palabra real como elemento y la otra tiene la longitud de la palabra como elemento.
¡Ahora podemos animar! Estamos usando la API de animación web porque usamos JavaScript puro aquí, no hay animaciones CSS para nosotros en este ejemplo.
Primero, animemos el cursor. Necesita parpadear intermitentemente. Necesitamos fotogramas clave y propiedades de animación, los cuales se almacenarán en su propio objeto JavaScript. Aquí están los fotogramas clave:
document.querySelector(".text_cursor").animate([
{
opacity: 0
},
{
opacity: 0, offset: 0.7
},
{
opacity: 1
}
], cursor_timings);
Hemos definido tres fotogramas clave como objetos que se almacenan en una matriz. El termino offset: 0.7
simplemente significa que después de completar el 70% de la animación, la opacidad pasará de 0 a 1.
Ahora, tenemos que definir las propiedades de la animación. Para eso, creemos un objeto JavaScript que los mantenga unidos:
let cursor_timings = {
duration: 700, // milliseconds (0.7 seconds)
iterations: Infinity, // number of times the animation will work
easing: 'cubic-bezier(0,.26,.44,.93)' // timing-function
}
Podemos darle un nombre a la animación, así:
let animation = document.querySelector(".text_cursor").animate([
// keyframes
], //properties);
Aquí hay una demostración de lo que hemos hecho hasta ahora:
¡Estupendo! Ahora, animemos el .text_hide
elemento que, fiel a su nombre, oculta el texto. Definimos propiedades de animación para este elemento:
let timings = {
easing: `steps(${Number(word_len[0])}, end)`,
delay: 2000, // milliseconds
duration: 2000, // milliseconds
fill: 'forwards'
}
El easing
La propiedad define cómo cambiará la velocidad de la animación con el tiempo. Aquí, hemos utilizado el steps()
función de temporización. Esto anima el elemento en segmentos discretos en lugar de una animación continua suave, ya sabes, para un movimiento de escritura más natural. Por ejemplo, la duración de la animación es de dos segundos, por lo que steps()
función anima el elemento en 9
pasos (un paso para cada personaje en “Animación”) durante dos segundos, donde cada paso tiene una duración de 2/9 = 0.22
segundos.
El end
El argumento hace que el elemento permanezca en su estado inicial hasta que se complete la duración del primer paso. Este argumento es opcional y su valor predeterminado se establece en end
. Si desea conocer en profundidad steps()
, entonces puedes consultar este increíble artículo de Joni Trythall.
El fill
la propiedad es la misma que animation-fill-mode
propiedad en CSS. Estableciendo su valor en forwards
, el elemento permanecerá en la misma posición definida por el último fotograma clave después de que se complete la animación.
A continuación, definiremos los fotogramas clave.
let reveal_animation_1 = document.querySelector(".text_hide").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0])}%` }
], timings);
Ahora mismo estamos animando solo una palabra. Más adelante veremos cómo animar varias palabras.
El último fotograma clave es crucial. Digamos que queremos animar la palabra “Animación”. Su longitud es 9
(ya que hay nueve caracteres) y sabemos que se almacena como una variable gracias a nuestro typing_animation()
función. La declaracion 100/text_len
resultados a 100/9
, o 11,11%, que es el ancho de todos y cada uno de los caracteres de la palabra “Animación”. Eso significa que el ancho de todos y cada uno de los caracteres es el 11,11% del ancho de toda la palabra. Si multiplicamos este valor por la longitud de la primera palabra (que en nuestro caso es 9
), luego obtenemos el 100%. Sí, podríamos haber escrito directamente al 100% en lugar de hacer todas estas cosas. Pero esta lógica nos ayudará cuando estemos animando varias palabras.
El resultado de todo esto es que el .text_hide
elemento animado de left: 0%
a left: 100%
. En otras palabras, el ancho de este elemento disminuye del 100% al 0% a medida que avanza.
Tenemos que agregar la misma animación al .text_cursor
elemento también porque queremos que haga la transición de izquierda a derecha junto con el .text_hide
elemento.
¡Yayy! Animamos una sola palabra. ¿Y si queremos animar varias palabras? Hagámoslo a continuación.
Animando varias palabras
Digamos que tenemos dos palabras que queremos escribir, tal vez “Animación de escritura”. Animamos la primera palabra siguiendo el mismo procedimiento que hicimos la última vez. Esta vez, sin embargo, cambiaremos el valor de la función de aceleración en las propiedades de la animación.
let timings = {
easing: `steps(${Number(word_len[0] + 1)}, end)`,
delay: 2000,
duration: 2000,
fill: 'forwards'
}
Hemos aumentado el número en un paso. ¿Por qué? Bueno, ¿qué pasa con un solo espacio después de una palabra? Debemos tener eso en cuenta. Pero, ¿y si solo hay una palabra en una oración? Para eso, escribiremos un if
condición donde, si el número de palabras es igual a 1, entonces steps(${Number(word_len[0])}, end)
. Si el número de palabras no es igual a 1, entonces steps(${Number(word_len[0] + 1)}, end)
.
function typing_animation() {
let text_element = document.querySelector(".text");
let text_array = text_element.innerHTML.split("");
let all_words = text_element.innerHTML.split(" ");
let text_len = text_array.length;
const word_len = all_words.map((word) => {
return word.length;
})
let timings = {
easing: `steps(${Number(word_len[0])}, end)`,
delay: 2000,
duration: 2000,
fill: 'forwards'
}
let cursor_timings = {
duration: 700,
iterations: Infinity,
easing: 'cubic-bezier(0,.26,.44,.93)'
}
document.querySelector(".text_cursor").animate([
{
opacity: 0
},
{
opacity: 0, offset: 0.7
},
{
opacity: 1
}
], cursor_timings);
if (all_words.length == 1) {
timings.easing = `steps(${Number(word_len[0])}, end)`;
let reveal_animation_1 = document.querySelector(".text_hide").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0])}%` }
], timings);
document.querySelector(".text_cursor").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0])}%` }
], timings);
} else {
document.querySelector(".text_hide").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0] + 1)}%` }
], timings);
document.querySelector(".text_cursor").animate([
{ left: '0%' },
{ left: `${(100 / text_len) * (word_len[0] + 1)}%` }
], timings);
}
}
typing_animation();
Para más de una palabra, usamos un for
bucle para iterar y animar cada palabra que sigue a la primera palabra.
for(let i = 1; i < all_words.length; i++){
// code
}
¿Por qué tomamos i = 1
? Porque para cuando este for
se ejecuta el bucle, la primera palabra ya ha sido animada.
A continuación, accederemos a la longitud de la palabra respectiva:
for(let i = 1; i < all_words.length; i++){
const single_word_len = word_len[i];
}
También definamos las propiedades de animación para todas las palabras que vienen después de la primera.
// the following code goes inside the for loop
let timings_2 = {
easing: `steps(${Number(single_word_len + 1)}, end)`,
delay: (2 * (i + 1) + (2 * i)) * (1000),
duration: 2000,
fill: 'forwards'
}
Lo más importante aquí es el delay
propiedad. Como saben, para la primera palabra, simplemente tuvimos la delay
propiedad establecida en dos segundos; pero ahora tenemos que aumentar el retraso de las palabras que siguen a la primera palabra de forma dinámica.
La primera palabra tiene un retraso de dos segundos. La duración de su animación también es de dos segundos que, juntos, suman cuatro segundos en total. Pero debe haber un intervalo entre la animación de la primera y la segunda palabra para que la animación sea más realista. Lo que podemos hacer es agregar una demora de dos segundos entre cada palabra en lugar de una. Eso hace que el retraso general de la segunda palabra 2 + 2 + 2
, o seis segundos. Del mismo modo, el retraso total para animar la tercera palabra es de 10 segundos, y así sucesivamente.
La función para este patrón es algo como esto:
(2 * (i + 1) + (2 * i)) * (1000)
… Donde multiplicamos por 1000 para convertir segundos en milisegundos.
Longitud de la palabra | Duración que toma un personaje para animar |
6 | 2/6 = 0,33 segundos |
8 | 2/8 = 0,25 segundos |
9 | 2/9 = 0,22 segundos |
12 | 2/12 = 0,17 segundos |
* La duración total es de 2 segundos
Cuanto más larga sea la palabra, más rápido se revelará. ¿Por qué? Porque la duración sigue siendo la misma por muy larga que sea la palabra. Juega con las propiedades de duración y retraso para hacer las cosas bien.
Recuerda cuando cambiamos el steps()
valor teniendo en cuenta un solo espacio después de una palabra? De la misma forma, la última palabra de la oración no tiene espacio después de ella, por lo que deberíamos tenerlo en cuenta en otra if
declaración.
// the following code goes inside the for loop
if (i == (all_words.length - 1)) {
timings_2.easing = `steps(${Number(single_word_len)}, end)`;
let reveal_animation_2 = document.querySelector(".text_hide").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i]))}%` }
], timings_2);
document.querySelector(".text_cursor").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i]))}%` }
], timings_2);
} else {
document.querySelector(".text_hide").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i] + 1))}%` }
], timings_2);
document.querySelector(".text_cursor").animate([
{ left: `${left_instance}%` },
{ left: `${left_instance + ((100 / text_len) * (word_len[i] + 1))}%` }
], timings_2);
}
Que es eso left_instance
¿variable? No lo hemos discutido, sin embargo, es la parte más crucial de lo que estamos haciendo. Déjame explicarte.
0%
es el valor inicial de la primera palabra left
propiedad. Pero, el valor inicial de la segunda palabra debe ser igual al valor final de la primera palabra. left
El valor de la propiedad.
if (i == 1) {
var left_instance = (100 / text_len) * (word_len[i - 1] + 1);
}
word_len[i - 1] + 1
se refiere a la longitud de la palabra anterior (incluido un espacio en blanco).
Tenemos dos palabras, “Animación de escritura”. Lo que hace text_len
igual 16
lo que significa que cada carácter es 6.25% del ancho total (100/text_len = 100/16
) que se multiplica por la longitud de la primera palabra, 7
. Todas las matemáticas nos dan 43.75
que es, de hecho, el ancho de la primera palabra. En otras palabras, el ancho de la primera palabra es 43.75%
el ancho de toda la cuerda. Esto significa que la segunda palabra comienza a animarse desde donde terminó la primera.
Por último, actualice el left_instance
variable al final de la for
círculo:
left_instance = left_instance + ((100 / text_len) * (word_len[i] + 1));
¡Ahora puede ingresar tantas palabras como desee en HTML y la animación simplemente funciona!
Prima
¿Has notado que la animación solo se ejecuta una vez? ¿Qué pasa si queremos hacer un bucle infinito? Es posible:
Ahí vamos: una versión JavaScript más robusta de una animación de mecanografía. Es genial que CSS también tenga un enfoque (o incluso múltiples enfoques) para hacer el mismo tipo de cosas. CSS incluso podría ser el mejor enfoque en una situación determinada. Pero cuando necesitamos mejoras que van más allá de lo que CSS puede manejar, agregar algo de JavaScript funciona bastante bien. En este caso, agregamos soporte para todas las palabras, independientemente de la cantidad de caracteres que contengan, y la capacidad de animar varias palabras. Y, con un pequeño retraso adicional entre palabras, obtenemos una animación de aspecto súper natural.
Eso es todo, ¡espero que hayas encontrado esto interesante! Firmando.