1. Web Design
  2. HTML/CSS
  3. JavaScript for Designers

Cómo usar JavaScript para reproducir sonido (efecto de una máquina de escribir)

Scroll to top

Spanish (Español) translation by Carlos (you can also view the original English article)

Hoy vamos a crear el efecto de una máquina de escribir utilizando CSS y JavaScript. Esto ocurrirá cuando un usuario sitúe el puntero del ratón (hover) encima de la imagen de una postal. Y para mejorarlo aún más, usaremos JavaScript para reproducir un sonido de escritura en la página y lo sincronizaremos con las letras a medida que aparezcan.

Nota: Daremos por sentado que este efecto es solamente para pantallas de escritorio. Tratar su funcionalidad en pantallas móviles/táctiles está más allá del alcance de este ejercicio. Al final del tutorial, propondré dos maneras de optimizarlo para escenarios responsivos.

Nuestro efecto de máquina de escribir con JavaScript

¡Revisa la demostración final! Haz clic en el botón de sonido para activar el audio y coloca el puntero del ratón sobre cada postal para que veas el efecto en acción.

Please accept marketing cookies to load this content.

1. Empieza por descargar los recursos para la página.

Para este ejercicio, vamos a necesitar algunos elementos.

  • Así que primero, he tomado cuatro imágenes de Unsplash.
  • Luego, un ícono de sonido de iconmonstr.
The sound icon from iconmonstr
  • Después, el efecto de sonido de una máquina de escribir antigua de Mixkit
The old typewriter typing sound from MixkitThe old typewriter typing sound from MixkitThe old typewriter typing sound from Mixkit
  • Por último, una fuente de máquina de escribir personalizada (web font) de Envato Elements.
Merchant Ledger - Font PackMerchant Ledger - Font PackMerchant Ledger - Font Pack
Merchant Ledger | Paquete de fuentes

Como miembro Pro de CodePen, subiré estos archivos al Asset Manager.

¿Necesitas más fuentes de máquina de escribir?

Envato Elements tiene una enorme colección de fuentes con estilos de máquina de escribir para ayudarte a personalizar este proyecto. ¡Suscríbete hoy y obtén descargas ilimitadas!

typewriter web fonts on envato elementstypewriter web fonts on envato elementstypewriter web fonts on envato elements

2. Continúa con el marcado de la página

El marcado de la página consistirá de los siguientes elementos:

  • Un elemento div que contendrá un título y un botón. La clase del botón determinará si se reproducirá un sonido cada vez que situemos el puntero del ratón sobre una imagen. De manera predeterminada, el sonido no se reproduce. Para activarlo, tenemos que hacer clic en el botón.
  • Un elemento div que contendrá cuatro imágenes junto con su texto. De manera predeterminada, todos los textos serán invisibles. Cada uno de ellos aparecerá en cuanto coloquemos el puntero del ratón sobre la imagen asociada. Lo ideal es que sean cortos.
  • Un elemento audio que se encargará de integrar el sonido en la página. Aunque es opcional, le daremos preload="auto" para informar a los navegadores que se debe cargar al cargar la página.

Aquí está el marcado de la página:

1
<div class="grid grid-btn">
2
  <h1>...</h1>
3
  <button type="button" class="btn-sound btn-sound-off" aria-label="Enable sound">
4
    <svg width="24" height="24" aria-hidden="true">
5
      <path d="M15 23l-9.309-6h-5.691v-10h5.691l9.309-6v22zm-9-15.009v8.018l8 5.157v-18.332l-8 5.157zm14.228-4.219c2.327 1.989 3.772 4.942 3.772 8.229 0 3.288-1.445 6.241-3.77 8.229l-.708-.708c2.136-1.791 3.478-4.501 3.478-7.522s-1.342-5.731-3.478-7.522l.706-.706zm-2.929 2.929c1.521 1.257 2.476 3.167 2.476 5.299 0 2.132-.955 4.042-2.476 5.299l-.706-.706c1.331-1.063 2.182-2.729 2.182-4.591 0-1.863-.851-3.529-2.184-4.593l.708-.708zm-12.299 1.299h-4v8h4v-8z" />
6
    </svg>
7
  </button>
8
</div>
9
<div class="grid grid-images">
10
  <figure>
11
    <img src="IMG_SRC" alt="IMG_ALT" width="IMG_WIDTH" height="IMG_HEIGHT">
12
    <figcaption class="animate">...</figcaption>
13
  </figure>
14
  
15
  <!-- more figure elements here -->
16
</div>
17
<audio preload="auto">
18
  <source src="AUDIO_SRC" type="audio/wav">
19
</audio>

3. Define los estilos

A continuación, trataremos los aspectos más importantes de nuestros estilos. En pro de la simplicidad, al ser JavaScript nuestro objetivo principal, no cubriremos aquí los estilos de restablecimiento (reset). Así que, vayamos directo al grano:

  • Usaremos CSS Grid para crear el diseño de dos divs de envoltura.
  • Usaremos el pseudoelemento ::before del botón de conmutación (sonido) para indicar que el sonido está apagado. Al principio, aparecerá esto.
  • El texto de las imágenes serán elementos posicionados de forma absoluta y serán invisibles por defecto. Dependiendo de la imagen, aparecerá en su parte superior o inferior.
  • Todos los caracteres de cada texto aparecerán secuencialmente en la imagen flotante. Esto se logrará gracias a la propiedad transition-delay que les añadiremos mediante JavaScript. Sin embargo, deberían desaparecer simultáneamente cuando quitemos el puntero del ratón de la imagen.
  • Agregaremos un espacio de 4 px a los spans vacíos (caracteres) de cada texto. Hasta el momento, nuestro marcado no contiene spans. Pero pronto los generaremos a través de JavaScript.

Aquí están nuestros estilos principales:

1
/*CUSTOM VARIABLES HERE*/
2
3
.grid {
4
  display: grid;
5
  max-width: 1200px;
6
  margin: 0 auto;
7
}
8
9
.grid-btn {
10
  grid-template-columns: auto auto;
11
  grid-gap: 20px;
12
  align-items: center;
13
  justify-content: center;
14
  margin-bottom: 30px;
15
}
16
17
.grid-images {
18
  grid-template-columns: 1fr 1fr;
19
  grid-gap: 20px 70px;
20
}
21
22
.grid-btn .btn-sound {
23
  position: relative;
24
  display: flex;
25
  padding: 5px;
26
  border: 2px solid var(--white);
27
}
28
29
.grid-btn .btn-sound-off::before {
30
  content: "";
31
  position: absolute;
32
  top: 50%;
33
  left: 0;
34
  width: 100%;
35
  border-top: 2px solid var(--white);
36
  transform: translateY(-50%) rotate(45deg);
37
}
38
39
.grid-btn .btn-sound svg {
40
  fill: var(--white);
41
}
42
43
.grid-images figure {
44
  position: relative;
45
  transform: rotate(5deg);
46
  transform-origin: bottom left;
47
  border: 10px solid var(--white);
48
  cursor: pointer;
49
  backface-visibility: hidden;
50
}
51
52
.grid-images figure:nth-child(even) {
53
  transform: rotate(-5deg);
54
  transform-origin: bottom right;
55
}
56
57
.grid-images img {
58
  display: block;
59
}
60
61
.grid-images .animate {
62
  position: absolute;
63
  top: 30px;
64
  left: 10px;
65
  right: 10px;
66
  display: flex;
67
  flex-wrap: wrap;
68
  justify-content: center;
69
  padding: 0 10px;
70
  overflow: hidden;
71
  text-align: center;
72
  mix-blend-mode: difference;
73
}
74
75
.grid-images figure:last-child .animate {
76
  top: auto;
77
  bottom: 30px;
78
}
79
80
.grid-images .animate span {
81
  font-size: clamp(18px, 2.5vw, 40px);
82
  opacity: 0;
83
  transition: all 0.01s ease-in-out;
84
}
85
86
.grid-images .animate span:empty {
87
  margin: 0 4px;
88
}
89
90
.grid-images figure:hover .animate span {
91
  opacity: 1;
92
}
93
94
.grid-images figure:not(:hover) .animate span[style] {
95
  transition-delay: 0s !important;
96
}

4. Añade la interactividad

Empezaremos por crear un bucle por todos lo elementos figure y para cada uno de ellos, se realizarán varias acciones:

1
const figures = document.querySelectorAll(".grid-images figure");
2
3
for (const figure of figures) {
4
  generateCharactersMarkup(figure);
5
  figure.addEventListener("mouseenter", mouseenterHandler);
6
  figure.addEventListener("mouseleave", mouseleaveHandler);
7
}

Nota: Para mayor seguridad, quizá debas ejecutar estas acciones al cargar la página (escucha el evento load).

Modifica el marcado de los textos

Primero, llamaremos a la función generateCharactersMarkup() y le pasaremos el elemento figure correspondiente.

Está es la declaración de la función:

1
function generateCharactersMarkup(el) {
2
  let index = 0;
3
  const textBlock = el.querySelector(".animate");
4
  const characters = textBlock.textContent.split("");
5
  const charactersHTML = characters
6
    .map(function (character) {
7
      let markup = "";
8
      if (character == " ") {
9
        markup = "<span></span>";
10
        if (index == 0) index = 1;
11
      } else {
12
        let style = "";
13
        if (index != 0) {
14
          const sec = 0.15 * index;
15
          const secRounded = Math.round(sec * 100) / 100;
16
          style = `style="transition-delay:${secRounded}s"`;
17
        }
18
        markup = `<span ${style}>${character}</span>`;
19
        index++;
20
      }
21
      return markup;
22
    })
23
    .join(" ");
24
  textBlock.innerHTML = charactersHTML;
25
}

Dentro de esta función:

  • Encontraremos el texto de la imagen y crearemos un bucle a través de sus caracteres.
  • Envolveremos cada carácter alrededor de un elemento span.
  • Comprobaremos si el carácter es el primero o es igual al espacio.
  • Si este no es el caso, recibirá la propiedad transition-delay. El valor de esta propiedad especificará la cantidad de tiempo de espera antes de mostrar el carácter objetivo. En nuestro ejemplo, cada transición se producirá después de 150 ms del inicio de la anterior. En tus proyectos, puedes ajustar este número mediante la variable sec.

Así que, si empezamos con este marcado:

1
<figcaption class="animate">Welcome to Iceland!</figcaption>

Después de ejecutar la función, se verá de la siguiente manera:

The generated markupThe generated markupThe generated markup

Observa que el primer carácter y los espacios no reciben un estilo en línea.

Naturalmente, en vez de generar este marcado mediante programación, podríamos haberlo añadido de manera predeterminada en nuestro HTML. Pero esto tendría dos desventajas. En primer lugar, se abultaría el HTML. En segundo lugar, se dificultaría su mantenimiento en caso de que se requiera algún cambio.

Reproducir el sonido con JavaScript

En este punto, pongamos el sonido en el bucle.

Recuerda que estará desactivado (off) de forma predeterminada.

The sound will be off from the beginning

Su estado será controlado por la clase btn-sound-off de CSS y la bandera sound de JavaScript.

Cada vez que hagamos clic en el botón, los dos se actualizarán. Por ejemplo, si el sonido está activado, el botón no contendrá esta clase y el valor de esta bandera será true.

Aquí está el código JavaScript relacionado:

1
const btnSound = document.querySelector(".grid-btn .btn-sound");
2
let sound = false;
3
4
btnSound.addEventListener("click", function () {
5
  sound = !sound;
6
  const ariaLabel =
7
    this.getAttribute("aria-label") == "Enable sound"
8
      ? "Disable sound"
9
      : "Enable sound";
10
  this.setAttribute("aria-label", ariaLabel);
11
  this.classList.toggle("btn-sound-off");
12
});

Cuando el sonido está activado, debería reproducirse al colocar el puntero sobre la imagen. ¿Pero por cuánto tiempo? Eso es lo que debemos considerar. Pues bien, nuestro archivo de audio de demostración es bastante largo, unos 24 segundos. Obviamente, no queremos reproducirlo todo. Solo una pequeña parte hasta que finalice el efecto de la transición.

Para hacerlo de esta manera, primero tenemos que calcular la duración de este efecto. Teniendo eso en cuenta, a continución inicializaremos un temporizador que reproducirá el sonido durante este período de tiempo.

Para entenderlo mejor, revisemos el efecto de la primera imagen. Si revisas la consola de tu navegador, notarás que tu último carácter tiene un retraso de 2.4 segundos. Eso significa que el efecto total durará 2.4 segundos (no contamos la duración de la transición, pues es casi cero). Para mantener la sincronización entre el efecto de la transición y el tiempo de reproducción del audio, reproduciremos los primeros 2.4 segundos del audio y luego lo pausaremos.

Para esta implementación, utilizaremos el evento mouseenter. Aquí está la declaración de su callback:

1
...
2
let timer;
3
4
function mouseenterHandler() {
5
  if (sound) {
6
    const spans = this.querySelectorAll(".animate span[style]");
7
    const duration = spans.length * 0.15 * 1000;
8
    /*SECOND METHOD FOR CALCULATING THE AUDIO DURATION BASED ON THE EFFECT DURATION*/
9
    /*const style = window.getComputedStyle(

10
      this.querySelector(".animate span[style]:last-child")

11
    );

12
    const duration =

13
      style.getPropertyValue("transition-delay").split("s")[0] * 1000;*/
14
    clearTimeout(timer);
15
    audio.play();
16
    timer = setTimeout(function () {
17
      audio.pause();
18
    }, duration);
19
  }
20
}

Observa en el código anterior las diferentes formas que podemos usar para calcular la duración del audio.

Por otra parte, el sonido debería hacer una pausa y volver a su posición inicial al quitar el puntero del ratón.

Para esta implementación, usaremos el evento mouseleave. Aquí está la declaración de su callback:

1
...
2
3
function mouseleaveHandler() {
4
  if (sound) {
5
    audio.pause();
6
    audio.currentTime = 0;
7
  }
8
}

Conclusión

Uf, ¡eso es todo amigos! Gracias por acompañarme en este largo recorrido. Hoy hemos cubierto muchas cosas. No solo creamos un genial efecto de escritura al colocar el puntero del ratón, sino que también lo hicimos más realista al sincronizarlo con el sonido de una antigua máquina de escribir.

Aquí está un recordatorio de lo que creamos:

Please accept marketing cookies to load this content.

Dispositivos móviles y táctiles

Como lo comentamos en la introducción, este efecto no está optimizado para dispositivos móviles/táctiles. Aquí hay dos posibles soluciones que podrías probar:

  • Desactivar el efecto en estas interfaces. Por ejemplo, escribir algún código para la detección de pantallas táctiles y, en ese caso, mostrar el texto de manera predeterminada.
  • De lo contrario, si deseas mantener el efecto para todos los dispositivos, asegúrate de capturar los eventos de toque (touch events).

Por supuesto, dependiendo de tus necesidades, puedes desacoplar el efecto de los eventos del mouse y mantenerlo  independiente o como parte de otros eventos como el de scroll.

Y por último, ten presente que este efecto podría no funcionar en todas las circunstancias sin una personalización adicional. Por ejemplo, como se comentó anteriormente, el texto de de las imágenes no deben ser largos y divididos en varias líneas como esto.

Como siempre, ¡muchas gracias por leer!

Más tutoriales de JavaScript en Tuts+