audioSyncLyric.ts 3.15 KB
import RabbitLyrics from 'rabbit-lyrics';
import parseLyrics from 'rabbit-lyrics/src/parseLyrics';

// @ts-ignore
export default class AudioSyncLyric extends RabbitLyrics {
  public startTime = 0;

  public setStartTime(time: number) {
    this.startTime = time || 0;
    this.render();
    this.mediaElement.addEventListener('timeupdate', this.synchronize);
  }

  private render(): void {
    // Add class names
    this.lyricsElement.classList.add('rabbit-lyrics');
    this.lyricsElement.classList.add(`rabbit-lyrics--${this.viewMode}`);
    this.lyricsElement.classList.add(`rabbit-lyrics--${this.alignment}`);
    this.lyricsElement.textContent = null;

    // Render lyrics lines
    this.lyricsLines = parseLyrics(this.lyrics).map((line) => {
      const lineElement = document.createElement('div');
      lineElement.className = 'rabbit-lyrics__line';
      lineElement.addEventListener('click', () => {
        this.mediaElement.currentTime = line.startsAt - this.startTime;
        this.synchronize();
      });
      const lineContent = line.content.map((inline) => {
        const inlineElement = document.createElement('span');
        inlineElement.className = 'rabbit-lyrics__inline';
        inlineElement.textContent = inline.content;
        lineElement.append(inlineElement);
        return { ...inline, element: inlineElement };
      });
      this.lyricsElement.append(lineElement);
      return { ...line, content: lineContent, element: lineElement };
    });
    this.synchronize();
  }

  private synchronize = () => {
    const time = this.startTime + this.mediaElement.currentTime;
    let changed = false; // If here are active lines changed
    const activeLines = this.lyricsLines.filter((line) => {
      if (time >= line.startsAt && time < line.endsAt) {
        // If line should be active
        if (!line.element.classList.contains('rabbit-lyrics__line--active')) {
          // If it hasn't been activated
          changed = true;
          line.element.classList.add('rabbit-lyrics__line--active');
        }
        line.content.forEach((inline) => {
          if (time >= inline.startsAt) {
            inline.element.classList.add('rabbit-lyrics__inline--active');
          } else {
            inline.element.classList.remove('rabbit-lyrics__inline--active');
          }
        });
        return true;
      }
      // If line should be inactive
      if (line.element.classList.contains('rabbit-lyrics__line--active')) {
        // If it hasn't been deactivated
        changed = true;
        line.element.classList.remove('rabbit-lyrics__line--active');
        line.content.forEach((inline) => {
          inline.element.classList.remove('rabbit-lyrics__inline--active');
        });
      }
      return false;
    });

    if (changed && activeLines.length > 0) {
      // Calculate scroll top. Vertically align active lines in middle
      const activeLinesOffsetTop =
        (activeLines[0].element.offsetTop +
          activeLines[activeLines.length - 1].element.offsetTop +
          activeLines[activeLines.length - 1].element.offsetHeight) /
        2;
      this.lyricsElement.scrollTop = activeLinesOffsetTop - this.lyricsElement.clientHeight / 2;
    }
  };
}