<div class="grid grid-btn">
  <h1>Hover over the images</h1>
  <button type="button" class="btn-sound btn-sound-off" aria-label="Enable sound">
    <svg width="24" height="24" aria-hidden="true">
      <path d="M6 7l8-5v20l-8-5v-10zm-6 10h4v-10h-4v10zm20.264-13.264l-1.497 1.497c1.847 1.783 2.983 4.157 2.983 6.767 0 2.61-1.135 4.984-2.983 6.766l1.498 1.498c2.305-2.153 3.735-5.055 3.735-8.264s-1.43-6.11-3.736-8.264zm-.489 8.264c0-2.084-.915-3.967-2.384-5.391l-1.503 1.503c1.011 1.049 1.637 2.401 1.637 3.888 0 1.488-.623 2.841-1.634 3.891l1.503 1.503c1.468-1.424 2.381-3.309 2.381-5.394z" />
    </svg>
  </button>
</div>
<div class="grid grid-images">
  <figure>
    <img src="https://assets.codepen.io/162656/iceland.jpg" alt="Iceland" width="640" height="427">
    <figcaption class="animate">Welcome to Iceland!</figcaption>
  </figure>

  <figure>
    <img src="https://assets.codepen.io/162656/greece.jpg" alt="Greece" width="640" height="427">
    <figcaption class="animate">Welcome to Greece!</figcaption>
  </figure>

  <figure>
    <img src="https://assets.codepen.io/162656/peru.jpg" alt="Peru" width="640" height="427">
    <figcaption class="animate">Welcome to Peru!</figcaption>
  </figure>

  <figure>
    <img src="https://assets.codepen.io/162656/vietnam.jpg" alt="Vietnam" width="640" height="427">
    <figcaption class="animate">Welcome to Vietnam!</figcaption>
  </figure>
</div>
<audio preload="auto"> 
  <source src="https://assets.codepen.io/162656/audio-old-typewriter.wav" type="audio/wav">
</audio>

<footer class="page-footer">
  <span>made by </span>
  <a href="https://georgemartsoukos.com/" target="_blank">
    <img width="24" height="24" src="https://assets.codepen.io/162656/george-martsoukos-small-logo.svg" alt="George Martsoukos logo">
  </a>
</footer>
/* RESET STYLES
–––––––––––––––––––––––––––––––––––––––––––––––––– */
@font-face {
  font-family: "Merchant Ledger";
  src: url("https://assets.codepen.io/162656/MerchantLedgerRegular.woff")
      format("woff"),
    url("https://assets.codepen.io/162656/MerchantLedgerRegular.woff2")
      format("woff2");
  font-weight: normal;
  font-style: normal;
}

:root {
  --black: #262626;
  --white: #fff;
}

* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
  font-weight: normal;
}

button {
  background: transparent;
  border: none;
  cursor: pointer;
  outline: none;
}

img {
  max-width: 100%;
  height: auto;
}

body {
  font-family: "Merchant Ledger";
  padding: 0 20px;
  margin: 50px auto 100px;
  text-align: center;
  color: var(--white);
  background: var(--black);
  min-height: 100vh;
}


/* MAIN STYLES
–––––––––––––––––––––––––––––––––––––––––––––––––– */
.grid {
  display: grid;
  max-width: 1200px;
  margin: 0 auto;
}

.grid-btn {
  grid-template-columns: auto auto;
  grid-gap: 20px;
  align-items: center;
  justify-content: center;
  margin-bottom: 30px;
}

.grid-images {
  grid-template-columns: 1fr 1fr;
  grid-gap: 20px 70px;
}

.grid-btn .btn-sound {
  position: relative;
  display: flex;
  padding: 5px;
  border: 2px solid var(--white);
}

.grid-btn .btn-sound-off::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  border-top: 2px solid var(--white);
  transform: translateY(-50%) rotate(45deg);
}

.grid-btn .btn-sound svg {
  fill: var(--white);
}

.grid-images figure {
  position: relative;
  transform: rotate(5deg);
  transform-origin: bottom left;
  border: 10px solid var(--white);
  cursor: pointer;
  backface-visibility: hidden;
}

.grid-images figure:nth-child(even) {
  transform: rotate(-5deg);
  transform-origin: bottom right;
}

.grid-images img {
  display: block;
}

.grid-images .animate {
  position: absolute;
  top: 30px;
  left: 10px;
  right: 10px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  padding: 0 10px;
  overflow: hidden;
  text-align: center;
  mix-blend-mode: difference;
}

.grid-images figure:last-child .animate {
  top: auto;
  bottom: 30px;
}

.grid-images .animate span {
  font-size: clamp(18px, 2.5vw, 40px);
  opacity: 0;
  transition: all 0.01s ease-in-out;
}

.grid-images .animate span:empty {
  margin: 0 4px;
}

.grid-images figure:hover .animate span {
  opacity: 1;
}

.grid-images figure:not(:hover) .animate span[style] {
  transition-delay: 0s !important;
}


/* FOOTER STYLES
–––––––––––––––––––––––––––––––––––––––––––––––––– */
.page-footer {
  position: fixed;
  right: 15px;
  bottom: 20px;
  display: flex;
  align-items: center;
  font-size: 1rem;
  padding: 5px;
}

.page-footer a {
  margin-left: 13px;
}
const figures = document.querySelectorAll(".grid-images figure");
const btnSound = document.querySelector(".grid-btn .btn-sound");
const noTransitionDelayClass = "no-transition-delay";
const audio = document.querySelector("audio");
let sound = false;
let timer;

for (const figure of figures) {
  generateCharactersMarkup(figure);
  figure.addEventListener("mouseenter", mouseenterHandler);
  figure.addEventListener("mouseleave", mouseleaveHandler);
}

function generateCharactersMarkup(el) {
  let index = 0;
  const textBlock = el.querySelector(".animate");
  const characters = textBlock.textContent.split("");
  const charactersHTML = characters
    .map(function (character) {
      let markup = "";
      if (character == " ") {
        markup = "<span></span>";
        if (index == 0) index = 1;
      } else {
        let style = "";
        if (index != 0) {
          const sec = 0.15 * index;
          const secRounded = Math.round(sec * 100) / 100;
          style = `style="transition-delay:${secRounded}s"`;
        }
        markup = `<span ${style}>${character}</span>`;
        index++;
      }
      return markup;
    })
    .join(" ");
  textBlock.innerHTML = charactersHTML;
}

function mouseenterHandler() {
  if (sound) {
    const spans = this.querySelectorAll(".animate span[style]");
    const duration = spans.length * 0.15 * 1000;
    /*SECOND METHOD FOR CALCULATING THE AUDIO DURATION BASED ON THE EFFECT DURATION*/
    /*const style = window.getComputedStyle(
      this.querySelector(".animate span[style]:last-child")
    );
    const duration =
      style.getPropertyValue("transition-delay").split("s")[0] * 1000;*/
    clearTimeout(timer);
    audio.play();
    timer = setTimeout(function () {
      audio.pause();
    }, duration);
  }
}

function mouseleaveHandler() {
  if (sound) {
    audio.pause();
    audio.currentTime = 0;
  }
}

btnSound.addEventListener("click", function () {
  sound = !sound;
  const ariaLabel =
    this.getAttribute("aria-label") == "Enable sound"
      ? "Disable sound"
      : "Enable sound";
  this.setAttribute("aria-label", ariaLabel);
  this.classList.toggle("btn-sound-off");
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.