Skip to content

歌词滚动效果

主要功能:歌词与进度联动

  • 歌词随进度滚动
  • 点击歌词跳转播放
  • 点击进度条歌词滚动到对应位置

效果

歌词滚动界面

思路整理

  1. 功能模块
  • 歌词展示
    • 歌曲 mp3 文件、歌词数据(网上搜索获取)
  • 播放器
    • HTML5 audio 标签
  1. 实现方法
  • 根据歌词文件的数据动态生成歌词

    • 将歌词数据的格式转化处理

      • js
        parseLrc(data); // '[00:24.50]听说白雪公主在逃跑' => {time: '00:24.50', lrc: '听说白雪公主在逃跑'}
        parseLrc(data); // '[00:24.50]听说白雪公主在逃跑' => {time: '00:24.50', lrc: '听说白雪公主在逃跑'}
      • js
        parseTime(time); //'01:24.50' => 84.50
        parseTime(time); //'01:24.50' => 84.50
    • 使用 li 创建歌词, 并将time转化好,用dataset保存至标签属性,之后方便获取

    • 多个元素一起创建,使用 createElementFragment,效率相对高些

  • 给歌词和播放器注册监听事件

    • 点击歌词跳转高亮并滚动,并进入播放状态
    • 点击播放器进度,找出对应歌词,并滚动至对应位置

目录

base
📦lyrics-roll
┣ 📜index.html
┣ 📜index.css
┣ 📜index.js
┣ 📜lrc.js
┗ 📜 童话镇.mp3
📦lyrics-roll
┣ 📜index.html
┣ 📜index.css
┣ 📜index.js
┣ 📜lrc.js
┗ 📜 童话镇.mp3
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./index.css">
    <title>播放器</title>
  </head>

  <body>
    <div id="container">
      <main>
        <ul class="lrc-wrap"></ul>
      </main>
      <div class="control-wrap">
        <audio src="./童话镇.mp3" controls controlsList="nodownload noplaybackrate"></audio>
      </div>
    </div>

    <script src="./lrc.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./index.css">
    <title>播放器</title>
  </head>

  <body>
    <div id="container">
      <main>
        <ul class="lrc-wrap"></ul>
      </main>
      <div class="control-wrap">
        <audio src="./童话镇.mp3" controls controlsList="nodownload noplaybackrate"></audio>
      </div>
    </div>

    <script src="./lrc.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
css
* {
  margin: 0;
  padding: 0;
}

html,
body,
#container {
  width: 100%;
  height: 100vh;
}

#container {
  position: relative;
  display: flex;
  flex-direction: column;
  background: #282828;
}

main {
  position: relative;
  flex: 1;
  overflow: hidden;
}

main::after,
main::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 50%;
  background-image: linear-gradient(to top, #0000, #000c);
  pointer-events: none;
  z-index: 10;
}

main::before {
  top: unset;
  bottom: 0;
  transform: rotateX(180deg);
}

.lrc-wrap {
  box-sizing: border-box;
  transform: translateY(0px);
  position: relative;
  transition: transform 0.5s ease-out;
  padding-top: calc(50vh - 18px);
  padding-bottom: calc(50vh - 18px);
  height: 100vh;
  overflow: auto;
}

.lrc-wrap > li {
  text-align: center;
  list-style: none;
  font-size: 1.2em;
  line-height: 2.5;
  transition: 0.3s ease-in;
  color: rgba(245, 245, 245, 0.4);
  cursor: pointer;
  user-select: none;
  height: 36px;
}

.lrc-wrap > li.active {
  transform: scale(1.5);
  color: #f5f5f5;
}

.control-wrap {
  flex: 0 0 50px;
  background-color: #f1f3f4;
}

audio {
  width: 100%;
}
* {
  margin: 0;
  padding: 0;
}

html,
body,
#container {
  width: 100%;
  height: 100vh;
}

#container {
  position: relative;
  display: flex;
  flex-direction: column;
  background: #282828;
}

main {
  position: relative;
  flex: 1;
  overflow: hidden;
}

main::after,
main::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 50%;
  background-image: linear-gradient(to top, #0000, #000c);
  pointer-events: none;
  z-index: 10;
}

main::before {
  top: unset;
  bottom: 0;
  transform: rotateX(180deg);
}

.lrc-wrap {
  box-sizing: border-box;
  transform: translateY(0px);
  position: relative;
  transition: transform 0.5s ease-out;
  padding-top: calc(50vh - 18px);
  padding-bottom: calc(50vh - 18px);
  height: 100vh;
  overflow: auto;
}

.lrc-wrap > li {
  text-align: center;
  list-style: none;
  font-size: 1.2em;
  line-height: 2.5;
  transition: 0.3s ease-in;
  color: rgba(245, 245, 245, 0.4);
  cursor: pointer;
  user-select: none;
  height: 36px;
}

.lrc-wrap > li.active {
  transform: scale(1.5);
  color: #f5f5f5;
}

.control-wrap {
  flex: 0 0 50px;
  background-color: #f1f3f4;
}

audio {
  width: 100%;
}
js
/**
 * @description: 解析歌词文件
 * @param {*} data
 * @return {*}
 */
function parseLrc(data) {
  return data
    .split("\n")
    .map((str) => {
      var res = str.match(/^\[(?<time>.*)\](?<lrc>.*)?$/);
      return res ? { time: res.groups.time, lrc: res.groups.lrc || "" } : undefined;
    })
    .filter(Boolean);
}

/**
 * @description: 处理时间格式
 * @return {*} parseTime("02:49.23") => 169.23
 */
function parseTime(timeStr) {
  var arr = timeStr.split(":").map((_str) => parseFloat(_str));
  return +(arr[0] * 60 + arr[1]).toFixed(2);
}

function setActive(el) {
  if (!el || el.tagName.toLocaleLowerCase() != "li") return;
  var halfHeight = main.offsetHeight / 2;
  var offsetHeight = el.offsetTop;
  // lrcWrap.style.transform = `translateY(${-offsetHeight}px)`;
  lrcWrap.scroll({ top: offsetHeight - halfHeight });
  liList.forEach((li) => li.el.classList.remove("active"));
  el.classList.add("active");
  currentActiveEl = el;
}

/**
 * @description: 创建每行歌词
 * @param {*} data 处理好的歌词数据
 * @return {*} [{el, time }]
 */
function createElement(data) {
  return data.map((row) => {
    const li = document.createElement("li");
    const time = parseTime(row.time);
    li.innerText = row.lrc;
    li.dataset.time = time;
    return { el: li, time };
  });
}

/**
 * @description: 生成歌词
 * @param {*} data
 * @return {*}
 */
function appendLrc(data) {
  const fragment = document.createDocumentFragment();
  data.forEach((item) => fragment.appendChild(item.el));
  lrcWrap.appendChild(fragment);
}

/** @type {HTMLDivElement} */
const lrcWrap = document.querySelector(".lrc-wrap");
var liList = createElement(parseLrc(lrc));

/** @type {HTMLAudioElement} */
var audio = document.querySelector("audio");
var main = document.querySelector("main");
var currentActiveEl = null;

// 给歌词注册监听事件
lrcWrap.addEventListener("click", (e) => {
  setActive(e.target);
  const time = e.target.dataset.time;
  if (!time) return;
  // 设置播放位置
  audio.currentTime = +time;
  // 开始播放
  if (audio.paused) audio.play();
});

// 给播放器注册监听事件
audio.addEventListener("loadedmetadata", () => {
  console.log("loadedmetadata");
});
audio.addEventListener("timeupdate", () => {
  const currentTime = audio.currentTime;
  const el = findLrcEl(liList, currentTime);
  if (el && currentActiveEl != el) setActive(el);
});

/**
 * @description: 查找对应歌词li
 * @param {*} liList
 * @param {*} currentTime
 * @return {*} el
 */
function findLrcEl(liList, currentTime) {
  let idx = liList.findIndex((li) => li.time > currentTime);
  if (idx) {
    return liList[idx - 1].el;
  }
}

appendLrc(liList);
/**
 * @description: 解析歌词文件
 * @param {*} data
 * @return {*}
 */
function parseLrc(data) {
  return data
    .split("\n")
    .map((str) => {
      var res = str.match(/^\[(?<time>.*)\](?<lrc>.*)?$/);
      return res ? { time: res.groups.time, lrc: res.groups.lrc || "" } : undefined;
    })
    .filter(Boolean);
}

/**
 * @description: 处理时间格式
 * @return {*} parseTime("02:49.23") => 169.23
 */
function parseTime(timeStr) {
  var arr = timeStr.split(":").map((_str) => parseFloat(_str));
  return +(arr[0] * 60 + arr[1]).toFixed(2);
}

function setActive(el) {
  if (!el || el.tagName.toLocaleLowerCase() != "li") return;
  var halfHeight = main.offsetHeight / 2;
  var offsetHeight = el.offsetTop;
  // lrcWrap.style.transform = `translateY(${-offsetHeight}px)`;
  lrcWrap.scroll({ top: offsetHeight - halfHeight });
  liList.forEach((li) => li.el.classList.remove("active"));
  el.classList.add("active");
  currentActiveEl = el;
}

/**
 * @description: 创建每行歌词
 * @param {*} data 处理好的歌词数据
 * @return {*} [{el, time }]
 */
function createElement(data) {
  return data.map((row) => {
    const li = document.createElement("li");
    const time = parseTime(row.time);
    li.innerText = row.lrc;
    li.dataset.time = time;
    return { el: li, time };
  });
}

/**
 * @description: 生成歌词
 * @param {*} data
 * @return {*}
 */
function appendLrc(data) {
  const fragment = document.createDocumentFragment();
  data.forEach((item) => fragment.appendChild(item.el));
  lrcWrap.appendChild(fragment);
}

/** @type {HTMLDivElement} */
const lrcWrap = document.querySelector(".lrc-wrap");
var liList = createElement(parseLrc(lrc));

/** @type {HTMLAudioElement} */
var audio = document.querySelector("audio");
var main = document.querySelector("main");
var currentActiveEl = null;

// 给歌词注册监听事件
lrcWrap.addEventListener("click", (e) => {
  setActive(e.target);
  const time = e.target.dataset.time;
  if (!time) return;
  // 设置播放位置
  audio.currentTime = +time;
  // 开始播放
  if (audio.paused) audio.play();
});

// 给播放器注册监听事件
audio.addEventListener("loadedmetadata", () => {
  console.log("loadedmetadata");
});
audio.addEventListener("timeupdate", () => {
  const currentTime = audio.currentTime;
  const el = findLrcEl(liList, currentTime);
  if (el && currentActiveEl != el) setActive(el);
});

/**
 * @description: 查找对应歌词li
 * @param {*} liList
 * @param {*} currentTime
 * @return {*} el
 */
function findLrcEl(liList, currentTime) {
  let idx = liList.findIndex((li) => li.time > currentTime);
  if (idx) {
    return liList[idx - 1].el;
  }
}

appendLrc(liList);
js
const lrc = `[00:01.78]童话镇 - 许娜
[00:03.14]词:竹君
[00:05.23]曲:暗杠
[00:06.92]录制:文克津
[00:24.50]听说白雪公主在逃跑
[00:26.74]小红帽在担心大灰狼
[00:29.91]听说疯帽喜欢爱丽丝
[00:33.39]丑小鸭会变成白天鹅
[00:36.89]听说彼得潘总长不大
[00:40.24]杰克他有竖琴和魔法
[00:43.75]听说森林里有糖果屋
[00:47.23]灰姑娘丢了心爱的玻璃鞋
[00:51.13]只有睿智的河水知道
[00:54.04]白雪是因为贪玩跑出了城堡
[00:57.80]小红帽有件抑制自己
[01:01.02]变成狼的大红袍
[01:04.55]总有一条蜿蜒在童话镇里七彩的河
[01:08.68]沾染魔法的乖张气息
[01:13.55]却又在爱里曲折
[01:18.84]川流不息扬起水花
[01:22.18]又卷入一帘时光入水
[01:25.41]让所有很久很久以前
[01:29.01]都走到幸福结局的时刻
[01:48.70]听说睡美人被埋藏
[01:50.81]小人鱼在眺望金殿堂
[01:54.11]听说阿波罗变成金乌
[01:57.74]草原有奔跑的剑齿虎
[02:01.10]听说匹诺曹总说着谎
[02:04.72]侏儒怪拥有宝石满箱
[02:07.96]听说悬崖有颗长生树
[02:11.57]红鞋子不知疲倦地在跳舞
[02:15.03]只有睿智的河水知道
[02:18.28]睡美人逃避了生活的煎熬
[02:22.01]小人鱼把阳光抹成眼影
[02:25.09]投进泡沫的怀抱
[02:28.31]总有一条蜿蜒在童话镇里七彩的河
[02:35.56]沾染魔法的乖张气息
[02:40.60]却又在爱里曲折
[02:42.92]川流不息扬起水花
[02:45.99]又卷入一帘时光入水
[02:49.23]让所有很久很久以前
[02:53.11]都走到幸福结局的时刻
[02:56.34]总有一条蜿蜒在童话镇里梦幻的河
[03:03.43]分隔了理想分隔现实
[03:07.73]又在前方的山口汇合
[03:10.38]川流不息扬起水花
[03:13.86]又卷入一帘时光入水
[03:16.79]让所有很久很久以前
[03:20.11]都走到幸福结局的时刻
[03:24.19]又陌生
[03:33.22]`;
const lrc = `[00:01.78]童话镇 - 许娜
[00:03.14]词:竹君
[00:05.23]曲:暗杠
[00:06.92]录制:文克津
[00:24.50]听说白雪公主在逃跑
[00:26.74]小红帽在担心大灰狼
[00:29.91]听说疯帽喜欢爱丽丝
[00:33.39]丑小鸭会变成白天鹅
[00:36.89]听说彼得潘总长不大
[00:40.24]杰克他有竖琴和魔法
[00:43.75]听说森林里有糖果屋
[00:47.23]灰姑娘丢了心爱的玻璃鞋
[00:51.13]只有睿智的河水知道
[00:54.04]白雪是因为贪玩跑出了城堡
[00:57.80]小红帽有件抑制自己
[01:01.02]变成狼的大红袍
[01:04.55]总有一条蜿蜒在童话镇里七彩的河
[01:08.68]沾染魔法的乖张气息
[01:13.55]却又在爱里曲折
[01:18.84]川流不息扬起水花
[01:22.18]又卷入一帘时光入水
[01:25.41]让所有很久很久以前
[01:29.01]都走到幸福结局的时刻
[01:48.70]听说睡美人被埋藏
[01:50.81]小人鱼在眺望金殿堂
[01:54.11]听说阿波罗变成金乌
[01:57.74]草原有奔跑的剑齿虎
[02:01.10]听说匹诺曹总说着谎
[02:04.72]侏儒怪拥有宝石满箱
[02:07.96]听说悬崖有颗长生树
[02:11.57]红鞋子不知疲倦地在跳舞
[02:15.03]只有睿智的河水知道
[02:18.28]睡美人逃避了生活的煎熬
[02:22.01]小人鱼把阳光抹成眼影
[02:25.09]投进泡沫的怀抱
[02:28.31]总有一条蜿蜒在童话镇里七彩的河
[02:35.56]沾染魔法的乖张气息
[02:40.60]却又在爱里曲折
[02:42.92]川流不息扬起水花
[02:45.99]又卷入一帘时光入水
[02:49.23]让所有很久很久以前
[02:53.11]都走到幸福结局的时刻
[02:56.34]总有一条蜿蜒在童话镇里梦幻的河
[03:03.43]分隔了理想分隔现实
[03:07.73]又在前方的山口汇合
[03:10.38]川流不息扬起水花
[03:13.86]又卷入一帘时光入水
[03:16.79]让所有很久很久以前
[03:20.11]都走到幸福结局的时刻
[03:24.19]又陌生
[03:33.22]`;