<!-- オーバーレイ / Overlay -->
<div id="overlay">
  <p><span class="far">&#xf254;</span>now loading...</p>
</div>

<!-- ヘッダ / Header -->
<div id="header">
  <!-- 再生コントロール / Playback control -->
  <div id="control" class="far">
    <a href="#" id="play" class="disabled">&#xf144;</a>
    <a href="#" id="stop" class="disabled">&#xf28d;</a>
  </div>
  <!-- アーティストと楽曲の情報 / Artist and song info -->
  <div id="meta">
    <div id="artist">artist: <span>-</span></div>
    <div id="song">song: <span>-</span></div>
  </div>
</div>

<!-- 音源 / Audio souce -->
<div id="media"></div>

<!-- 歌詞 / Lyrics text -->
<div id="lyrics">
  <!-- 文字 / Text -->
  <div id="text"></div>
  <!-- ビートバー / Beat bar -->
  <div id="bar"></div>
</div>
/* 背景 / Background */
body {
  background: #077;
  background-image: linear-gradient(0deg, rgba(0,119,119,1) 0%, rgba(0,177,160,1) 100%);
  background-attachment: fixed;
  background-size: 100vw 100vh;
}

/* オーバーレイ / Overlay */
#overlay {
  user-select: none;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  background: #0006;
  color: #fffc;
  z-index: 5;
}
#overlay.disabled {
  display: none
}
#overlay > p {
  width: 100vw;
  font-size: 40px;
  text-align: center;
}
#overlay > p > span {
  display: inline-block;
  padding-right: 20px;
  margin-right: 20px;
  border-right: 1px solid #fff6;
}

/* フッターと音源 / Header and audio source */
#header,
#media {
  /* ページ左に固定 / Stick to the page left */
  position: fixed;
  left: 0;
  
  /* 背景色と文字色 / Background and text color */
  background: #000c;
  color: #fff;

  z-index: 1;
}

/* フッター / Footer */
#header {
  /* 上寄せ / Top-aligned */
  top: 10px;

  /* 少し余裕を持たせる / Box with a small padding */
  padding: 10px 16px;
  
  /* フォントサイズ小さめ、太め / Small but bold typography */
  font-size: 10.5px;
  font-weight: bold;
  
  /* 子要素を横に並べて配置 / Align child content to the right */
  display: flex;
  
  /* 子要素は縦に中央揃え / Vertically middle-aligned */
  align-items: center;
}

/* 再生ボタン / Play button */
#control {
  font-size: 21px;
  padding-right: 10px;
  border-right: 1px solid #fff9;
}
#control.disabled {
  display: none;
}
#control a {
  color: #aff;
  text-decoration: none;
}
#control a.disabled {
  opacity: 0.3;
}
#control a:hover {
  color: #dff;
}

/* アーティストと楽曲の情報 / Artist and song info */
#meta {
  padding-left: 10px;
}
#meta span {
  font-weight: normal;
}

/* 音源 / Audio source */
#media {
  /* 下寄せ / Bottom-aligned */
  bottom: 10px;
}

/**
 * ビート情報が取れるようになったらビートバーを表示
 * Show beat bar when beat information becomes available
 */
@keyframes activateBeatBar {
  0% {
    opacity: 0;
  }
  100% {
    width: 100%;
    opacity: 1;
  }
}

/**
 * ビート毎に右に広げてフェードアウト
 * Make beat bar span to the right and then fade out
 */
@keyframes showBeatBar {
  0% {
    width: 0;
    opacity: 1;
  }
  50% {
    width: 100%;
    opacity: 1;
  }
  100% {
    width: 100%;
    opacity: 0;
  }
}

/**
 * 歌詞が下からせり出してくる
 * Make lyrics text appear from the bottom
 */
@keyframes showLyrics {
  0% {
    transform: translate3d(0, 100%, 0);
  }
  100% {
    transform: translate3d(0, 0, 0);
  }
}

/* ビートバー / Beat bar */
#bar {
  opacity: 0;
  height: 10px;
  background: #aff;
}
#bar.active {
  animation activateBeatBar 0.3s: 
}
#bar.beat {
  animation: showBeatBar 0.5s;
}

/* 歌詞 / Lyrics */
#lyrics {
  z-index: 0;
  padding: 3em 0 5em 0;
  line-height: 1.6em;
  font-size: 36px;
  font-family: "M PLUS 1p";
  font-weight: bold;
  color: #aff;
  text-shadow: 2px 2px 3px #364;
  user-select: none;
  cursor: pointer;

  /* 歌詞をちょっと回転させる / Rotate text */
  transform: rotateX(10deg) rotateY(-20deg);
}
#text > div {
  /* 文字ごとに改行しない / No line-break per char */
  display: inline-block;
  /* 溢れた部分を隠す / Hide overflow content */
  overflow: hidden;
  /* 高さ指定で文字をあえて溢れさす / Make text overflow with height specified */
  height: 45px;
}
#text > div > div {
  animation: showLyrics 0.5s;
}

/**
 * 名詞などを強調表示する
 * Emphasize nouns
 */
#text .noun {
  color: #fcd;
  font-size: 40px;
}

/**
 * フレーズ終わりで右にマージンを空けて読みやすくする
 * Add right margin to the last char in phrases
 */
#text .lastChar {
  margin-right: 40px;
}
const { Player } = TextAliveApp;

// TextAlive Player を初期化
const player = new Player({
  app: true,
  mediaElement: document.querySelector("#media")

  // オプション一覧
  // https://developer.textalive.jp/packages/textalive-app-api/interfaces/playeroptions.html
});

const overlay = document.querySelector("#overlay");
const bar = document.querySelector("#bar");
const textContainer = document.querySelector("#text");
let b, c;

player.addListener({
  /* APIの準備ができたら呼ばれる */
  onAppReady(app) {
    if (app.managed) {
      document.querySelector("#control").className = "disabled";
    }
    if (!app.songUrl) {
      player.createFromSongUrl("http://www.youtube.com/watch?v=ygY2qObZv24");
    }
  },

  /* 楽曲が変わったら呼ばれる */
  onAppMediaChange() {
    // 画面表示をリセット
    overlay.className = "";
    bar.className = "";
    resetChars();
  },

  /* 楽曲情報が取れたら呼ばれる */
  onVideoReady(video) {
    // 楽曲情報を表示
    document.querySelector("#artist span").textContent =
      player.data.song.artist.name;
    document.querySelector("#song span").textContent = player.data.song.name;

    // 最後に表示した文字の情報をリセット
    c = null;
  },

  /* 再生コントロールができるようになったら呼ばれる */
  onTimerReady() {
    overlay.className = "disabled";
    document.querySelector("#control > a#play").className = "";
    document.querySelector("#control > a#stop").className = "";
  },

  /* 再生位置の情報が更新されたら呼ばれる */
  onTimeUpdate(position) {
    // 現在のビート情報を取得
    let beat = player.findBeat(position);
    if (b !== beat) {
      if (beat) {
        requestAnimationFrame(() => {
          bar.className = "active";
          requestAnimationFrame(() => {
            bar.className = "active beat";
          });
        });
      }
      b = beat;
    }

    // 歌詞情報がなければこれで処理を終わる
    if (!player.video.firstChar) {
      return;
    }

    // 巻き戻っていたら歌詞表示をリセットする
    if (c && c.startTime > position + 1000) {
      resetChars();
    }

    // 500ms先に発声される文字を取得
    let current = c || player.video.firstChar;
    while (current && current.startTime < position + 500) {
      // 新しい文字が発声されようとしている
      if (c !== current) {
        newChar(current);
        c = current;
      }
      current = current.next;
    }
  },

  /* 楽曲の再生が始まったら呼ばれる */
  onPlay() {
    const a = document.querySelector("#control > a#play");
    while (a.firstChild) a.removeChild(a.firstChild);
    a.appendChild(document.createTextNode("\uf28b"));
  },

  /* 楽曲の再生が止まったら呼ばれる */
  onPause() {
    const a = document.querySelector("#control > a#play");
    while (a.firstChild) a.removeChild(a.firstChild);
    a.appendChild(document.createTextNode("\uf144"));
  }
});

/* 再生・一時停止ボタン */
document.querySelector("#control > a#play").addEventListener("click", (e) => {
  e.preventDefault();
  if (player) {
    if (player.isPlaying) {
      player.requestPause();
    } else {
      player.requestPlay();
    }
  }
  return false;
});

/* 停止ボタン */
document.querySelector("#control > a#stop").addEventListener("click", (e) => {
  e.preventDefault();
  if (player) {
    player.requestStop();

    // 再生を停止したら画面表示をリセットする
    bar.className = "";
    resetChars();
  }
  return false;
});

/**
 * 新しい文字の発声時に呼ばれる
 * Called when a new character is being vocalized
 */
function newChar(current) {
  // 品詞 (part-of-speech)
  // https://developer.textalive.jp/packages/textalive-app-api/interfaces/iword.html#pos
  const classes = [];
  if (
    current.parent.pos === "N" ||
    current.parent.pos === "PN" ||
    current.parent.pos === "X"
  ) {
    classes.push("noun");
  }

  // フレーズの最後の文字か否か
  if (current.parent.parent.lastChar === current) {
    classes.push("lastChar");
  }

  // noun, lastChar クラスを必要に応じて追加
  const div = document.createElement("div");
  div.className = classes.join(" ");
  div.appendChild(document.createTextNode(current.text));

  // 文字を画面上に追加
  const container = document.createElement("div");
  container.appendChild(div);
  container.addEventListener("click", () => {
    player.requestMediaSeek(current.startTime);
  });
  textContainer.appendChild(container);
}

/**
 * 歌詞表示をリセットする
 * Reset lyrics view
 */
function resetChars() {
  c = null;
  while (textContainer.firstChild)
    textContainer.removeChild(textContainer.firstChild);
}

External CSS

  1. https://fonts.googleapis.com/css?family=M+PLUS+1p
  2. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/regular.min.css

External JavaScript

  1. https://unpkg.com/axios/dist/axios.min.js
  2. https://unpkg.com/textalive-app-api/dist/index.js