banner
NEWS LETTER

深入浅出:用 JavaScript 实现歌词同步滚动效果

Scroll down

深入浅出:用 JavaScript 实现歌词同步滚动效果

在现代 Web 音乐播放器中,与歌曲播放同步滚动的歌词是一个标志性功能。它极大地提升了用户体验。本文将详细拆解实现这一功能的两个关键技术点:LRC 歌词文件的解析让当前歌词始终保持在视图中央的滚动算法

核心原理

实现歌词同步滚动的本质在于建立 时间视图 之间的桥梁。

  1. 数据基础 (歌词解析):将带有时间戳的 LRC 歌词文本,解析成 JavaScript 易于操作的结构化数据(如对象数组)。
  2. 时间同步 (事件监听):利用 HTML5 <audio> 元素的 timeupdate 事件,实时获取当前播放进度。
  3. 视图更新 (DOM 操作):根据当前播放时间,找到对应的歌词行,为其添加高亮样式,并计算精确的滚动距离,使其平滑地移动到容器中心。

一、歌词解析:将文本转化为结构化数据

歌词数据通常以 LRC (LyRiCs) 格式存在。它的特点是每一行歌词前都带有一个或多个时间戳。

LRC 格式示例:

1
2
[00:16.81]故事的小黄花
[00:19.49]从出生那年就飘着

我们的目标是将这个文本字符串转换成一个对象数组,每个对象包含两个关键信息:time (以秒为单位) 和 text (歌词内容)。

目标数据结构:

1
2
3
4
5
[
{ time: 16.81, text: '故事的小黄花' },
{ time: 19.49, text: '从出生那年就飘着' },
// ...
]

实现 parseLrc 函数

我们可以编写一个函数,通过正则表达式来高效地完成解析任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 解析 LRC 格式的歌词字符串
* @param {string} lrc - 包含 LRC 歌词的字符串
* @returns {Array<Object>} 解析后的歌词对象数组,形如 [{ time, text }, ...]
*/
function parseLrc(lrc) {
const lines = lrc.split('\n');
const result = [];
// 匹配 [分:秒.毫秒] 的正则表达式
const timeRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;

for (const line of lines) {
const match = line.match(timeRegex);
if (match) {
// 提取时间部分
const minutes = parseInt(match[1], 10);
const seconds = parseInt(match[2], 10);
// 毫秒部分可能为两位或三位
const milliseconds = parseInt(match[3].padEnd(3, '0'), 10);

// 计算总秒数
const time = minutes * 60 + seconds + milliseconds / 1000;

// 提取歌词文本
const text = line.replace(timeRegex, '').trim();

// 只有当歌词文本不为空时才添加
if (text) {
result.push({ time, text });
}
}
}
// 返回按时间排序的数组(LRC文件通常已排序,此步为保险)
return result.sort((a, b) => a.time - b.time);
}

代码解析:

  1. 分割行lrc.split('\n') 将整个歌词字符串按行分割成数组。
  2. 正则表达式\[(\d{2}):(\d{2})\.(\d{2,3})\] 是核心。
    • \[\] 匹配方括号本身。
    • (\d{2}) 捕获两位数字(分钟和秒)。
    • (\d{2,3}) 捕获两位或三位的毫秒。
  3. 时间转换:将捕获到的分钟、秒、毫秒统一转换为总秒数,方便与 <audio> 元素的 currentTime 属性(单位也是秒)进行比较。
  4. 文本提取:使用 replace() 方法移除时间戳部分,再用 trim() 清除首尾空格,得到纯净的歌词文本。
  5. 构建结果:将 timetext 作为一个对象存入 result 数组。

二、保持居中滚动:滚动距离的精确计算

这是实现歌词滚动的视觉核心。我们的目标是:当某一行歌词被高亮时,整个歌词列表 (<ul>) 向上或向下滚动,使得高亮行 (<li>) 的垂直中心线与外部容器 (div) 的垂直中心线对齐。

HTML & CSS 准备

首先,需要一个固定的容器和可在其中滚动的列表。

HTML 结构:

1
2
3
4
5
<div class="lyrics-container" id="lyrics-container">
<ul class="lyrics-list" id="lyrics-list">
<!-- 歌词 li 将动态生成于此 -->
</ul>
</div>

关键 CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
.lyrics-container {
height: 300px; /* 固定高度,创建“视口” */
overflow: hidden; /* 隐藏超出容器的内容 */
}

.lyrics-list {
transition: transform 0.5s ease-out; /* 平滑滚动动画 */
}

.lyrics-list li.active {
color: #1DB954; /* 高亮样式 */
font-weight: bold;
}

overflow: hidden 是实现“滚动”效果的基石,而 transition 则让滚动变得平滑自然。

滚动算法与实现

timeupdate 事件触发时,我们找到当前应高亮的歌词行,然后执行以下计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ... 在 timeupdate 事件监听器中 ...

// 假设我们已经找到了当前高亮的 li 元素,名为 activeLi
const container = document.getElementById('lyrics-container');
const lyricsList = document.getElementById('lyrics-list');

// 1. 获取所需尺寸信息
const containerHeight = container.clientHeight; // 容器的可视高度
const activeLiHeight = activeLi.clientHeight; // 高亮 li 的高度
const activeLiOffsetTop = activeLi.offsetTop; // 高亮 li 相对于其父元素(ul)顶部的距离

// 2. 计算滚动偏移量 (offset)
// 目标:将 activeLi 的中心移动到 container 的中心
const offset = activeLiOffsetTop - (containerHeight / 2) + (activeLiHeight / 2);

// 3. 应用滚动
lyricsList.style.transform = `translateY(-${offset}px)`;

完整代码可到掘金-歌词滚动案例查看

核心公式剖析

offset = activeLi.offsetTop - (containerHeight / 2) + (activeLiHeight / 2)

让我们一步步理解这个公式:

  1. activeLi.offsetTop
    这是 <ul> 列表需要向上滚动的基本距离,以使得 activeLi顶部 与容器的 顶部 对齐。

  2. - (containerHeight / 2)
    在第一步的基础上,我们将整个 <ul> 列表 向下 移动容器高度的一半。此时,activeLi顶部 就对齐了容器的 中心线

  3. + (activeLiHeight / 2)
    最后,我们再将 <ul> 列表 向上 移动 activeLi 高度的一半。这会使 activeLi 自身向上移动半个身位,最终它的 中心线 就与容器的 中心线 完全重合了。

通过 transform: translateY(-${offset}px) 将计算出的偏移量应用到 <ul> 元素上,浏览器就会结合 CSS transition 属性,创造出我们想要的平滑居中滚动效果。使用 transform 而不是 margin-toptop,可以获得更好的渲染性能,因为它通常能触发 GPU 加速。

总结

实现一个优雅的歌词滚动功能,可以分解为以下几个清晰的步骤:

  1. 数据处理:通过正则表达式和字符串操作,将 LRC 文本解析为包含 timetext 的结构化数组。
  2. 事件驱动:监听 <audio>timeupdate 事件作为功能的核心驱动力。
  3. 状态查找:在事件回调中,遍历数据,找到当前时间对应的歌词行索引。
  4. 视图更新
    • 通过切换 CSS 类来 高亮 当前行。
    • 运用 offsetTop, clientHeight 等属性和居中滚动 核心公式,计算出精确的滚动偏移量。
    • 使用 transform: translateY() 高性能地 执行滚动

掌握了这两个核心技术点,你不仅能实现歌词滚动,还能将其中的原理应用到其他需要将数据与滚动视图同步的场景中。

其他文章
目录导航 置顶
  1. 1. 深入浅出:用 JavaScript 实现歌词同步滚动效果
  2. 2. 核心原理
  3. 3. 一、歌词解析:将文本转化为结构化数据
    1. 3.1. 实现 parseLrc 函数
  4. 4. 二、保持居中滚动:滚动距离的精确计算
    1. 4.1. HTML & CSS 准备
    2. 4.2. 滚动算法与实现
    3. 4.3. 核心公式剖析
  5. 5. 总结