Cómo usar JavaScript para reproducir sonido (efecto de una máquina de escribir)
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.
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.

- Después, el efecto de sonido de una máquina de escribir antigua de Mixkit.



- Por último, una fuente de máquina de escribir personalizada (web font) de Envato Elements.


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!



2. Continúa con el marcado de la página
El marcado de la página consistirá de los siguientes elementos:
- Un elemento
divque 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
divque 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
audioque se encargará de integrar el sonido en la página. Aunque es opcional, le daremospreload="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
::beforedel 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-delayque 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 contienespans. 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 variablesec.
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:



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.

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:
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+


JavaScriptIntroducción para diseñadores web a los Event Listeners de JavaScriptAnna Monus

JavaScriptHoja de referencia esencial: convierte jQuery a JavaScriptAnna Monus

JavaScriptCómo crear transiciones de página suaves con JavaScriptAdi Purdila

HTMLCrea una aplicación meteorológica simple con JavaScript puroGeorge Martsoukos

CSSCómo crear un efecto de escala de grises a color durante el desplazamiento (con CSS y JavaScript)George Martsoukos

JavaScriptCómo implementar el desplazamiento suave con JavaScript puroGeorge Martsoukos



