Beispiel:Karaoke-4.html

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche
<!DOCTYPE html>
<html lang="de">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" media="screen" href="./Beispiel:Grundlayout.css">
  <title>Karaoke – Scarborough Fair</title>

  <style>
    :root {
      --zeilen-hoehe: 4.2rem;
      --sichtbare-zeilen: 4;
      --aktiv-farbe: #fffd59;
      --inaktiv-deckkraft: 0.35;
      --uebergangs-dauer: 0.35s;
      --box-hintergrund: #1a1a2e;
      --box-textfarbe: #e0e0e0;
      --schriftgroesse: 2rem;
    }

    body {
      font-family: sans-serif;
      max-width: 60em;
      margin: 1em auto;
      padding: 0 1rem 3rem;
    }

    audio {
      width: 100%;
    }

    .karaoke-box {
      position: relative;
      margin-top: 1.5rem;
      background: var(--box-hintergrund);
      border-radius: .75rem;
      height: calc(var(--zeilen-hoehe) * var(--sichtbare-zeilen));
      overflow: hidden;
    }

    .karaoke-box::before,
    .karaoke-box::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      height: var(--zeilen-hoehe);
      z-index: 1;
      pointer-events: none;
    }

    .karaoke-box::before {
      top: 0;
      background: linear-gradient(to bottom, var(--box-hintergrund), transparent);
    }

    .karaoke-box::after {
      bottom: 0;
      background: linear-gradient(to top, var(--box-hintergrund), transparent);
    }

    .karaoke-lauf {
      transition: transform var(--uebergangs-dauer) ease;
    }

    .karaoke-zeile {
      height: var(--zeilen-hoehe);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: var(--schriftgroesse);
      color: var(--box-textfarbe);
      opacity: var(--inaktiv-deckkraft);
      transition: opacity var(--uebergangs-dauer) ease, color var(--uebergangs-dauer) ease;
      padding: 0 1.5rem;
      text-align: center;
      margin: 0;
    }

    .karaoke-zeile.aktiv {
      opacity: 1;
      color: var(--aktiv-farbe);
      font-weight: bold;
    }
  </style>
</head>

<body>
  <h1>🎤 Scarborough Fair</h1>
  <p>Karaoke-Player – Text scrollt mit, aktuelle Zeile ist markiert</p>

  <audio id="abspieler" controls>
    <source src="/images/c/c8/Scarborough_Fair.mp3" type="audio/mpeg">
    Dein Browser unterstützt das Audio-Element nicht.
  </audio>

  <div class="karaoke-box">
    <div class="karaoke-lauf" id="lauf"></div>
  </div>

  <script>
    (function () {
      const lrcInhalt = String.raw`[ar:Traditional]
[ti:Scarborough Fair]
[offset:0]

[00:00.50]Are you going to Scarborough Fair?
[00:04.50]Parsley, sage, rose- mary and thyme,
[00:09.50]Remember me to one who lives there,
[00:15.50]She once was a true love of mine.
[00:23.50]Tell her to make me a cambric shirt,
[00:28.50]Parsley, sage, rose- mary and thyme,
[00:33.50]Without no seams nor needle- work,
[00:38.50]Then she'll be a true love of mine.
[00:47.50]Tell her to find me an acre of land,
[00:52.50]Parsley, sage, rose- mary and thyme,
[00:57.50]Between the salt water and the sea strands,
[01:02.50]Then she'll be a true love of mine.
[01:11.00]Tell her to plough it with a sickle of leather,
[01:16.00]Parsley, sage, rose- mary and thyme,
[01:21.00]And bind it all with a bunch of heather,
[01:26.00]Then she'll be a true love of mine.
[01:35.00]Are you going to Scarborough Fair?
[01:40.00]Parsley, sage, rose- mary and thyme,
[01:45.00]Remember me to the one who lives there,
[01:50.00]She once was a true love of mine.`;

      function lrcParsen(rohtext) {
        const zeitMuster = /\[(\d{2}):(\d{2}\.\d+)\](.+)/;
        return rohtext
          .split("\n")
          .map(zeile => {
            const treffer = zeile.match(zeitMuster);
            if (!treffer) return null;
            const minuten = parseFloat(treffer[1]);
            const sekunden = parseFloat(treffer[2]);
            return { zeit: minuten * 60 + sekunden, text: treffer[3].trim() };
          })
          .filter(Boolean)
          .sort((a, b) => a.zeit - b.zeit);
      }

      const zeilen = lrcParsen(lrcInhalt);
      const lauf = document.getElementById("lauf");

      const abstandOben = document.createElement("div");
      abstandOben.style.height = "var(--zeilen-hoehe)";
      lauf.appendChild(abstandOben);

      const zeilenElemente = zeilen.map(({ text }) => {
        const el = document.createElement("p");
        el.className = "karaoke-zeile";
        el.textContent = text;
        lauf.appendChild(el);
        return el;
      });

      const abstandUnten = document.createElement("div");
      abstandUnten.style.height = "var(--zeilen-hoehe)";
      lauf.appendChild(abstandUnten);

      const zeilenHoehe = zeilenElemente[0].getBoundingClientRect().height;
      const zeitstempel = zeilen.map(z => z.zeit);

      function aktivenIndexFinden(aktuelleZeit) {
        for (let i = zeitstempel.length - 1; i >= 0; i--) {
          if (aktuelleZeit >= zeitstempel[i]) return i;
        }
        return -1;
      }

      function zuIndexScrollen(index) {
        lauf.style.transform = `translateY(${-index * zeilenHoehe}px)`;
      }

      let aktiverIndex = -1;
      const audio = document.getElementById("abspieler");

      function karaokeAktualisieren() {
        const neuerIndex = aktivenIndexFinden(audio.currentTime);
        if (neuerIndex === aktiverIndex) return;

        if (aktiverIndex >= 0) zeilenElemente[aktiverIndex].classList.remove("aktiv");

        if (neuerIndex >= 0) {
          zeilenElemente[neuerIndex].classList.add("aktiv");
          zuIndexScrollen(neuerIndex);
        }

        aktiverIndex = neuerIndex;
      }

      audio.addEventListener("timeupdate", karaokeAktualisieren);
      audio.addEventListener("seeked", karaokeAktualisieren);
      audio.addEventListener("play", karaokeAktualisieren);
    })();
  </script>
</body>
</html>