歌词跟随歌曲播放效果
作者: 更新: 3/23/2025 字数: 0 字 时长: 0 分钟
序言
先说明一下,本篇这个歌词跟随播放效果并不难,我个人给它定位是 初级+ 的水平。大概花了几个小时吧就写完了,时间大部分耗在 corner case 上。
在做一个功能前,我们首先最需要克服的是恐惧心理,不要总感觉 "这该怎么办?" 或者急急忙忙的去网上翻找类似组件然后磕磕盼盼的粗糙完成。这很难得到真正的成长,也不利于养成程序员思维。
---- 致年轻时候的自己
正文
废话不多说,先看效果:
首先对这个功能进行列举拆分,由大化小。主要有:
- 一个播放器控件,负责播放。当然播放的歌曲、歌词资源也要自己去找一个
- 一个面板容器,负责展示歌词
- 歌词面板从顶部到中间有一个半透明到完全显示的过渡效果
- 播放歌曲的时候,歌词会滚动到对应行且高亮该行
先看播放器,这个是最简单的,直接用浏览器自带的
html
<audio id="audio" controls
src="./song/assets/我记得你眼里的依恋.mp3" preload="metadata">
</audio>
INFO
注意:audio 控件本身是不显示,页面上显示的是其 controls
容器面板,我们设置一个定高满宽内容居中就行了。 那歌词是如何显示呢?歌词的原始数据是这样的:
txt
[00:26.67]走在红尘俗世间
[00:30.39]谁的呼唤飘在耳边
[00:34.89]那么熟悉却又遥远
[00:39.57]为什么痴心两处总难相见
[00:46.53]徘徊在起风的午夜
[00:50.19]谁的叹息飘在风间
[00:54.69]那么无奈却又无悔
[00:59.13]多少前世残梦留待今生缘
[01:04.02]就算换了时空变了容颜
[01:10.08]我依然记得你眼里的依恋
[01:14.73]纵然聚散由命也要用心感动天
[01:22.80]就算换了时空变了容颜
[01:28.77]我依然记得你眼里的依恋
[01:33.45]纵然难续前世也要再结今生缘
[02:02.34]走在红尘俗世间
// ... 更多歌词
我们把它转化一下,形成 “秒数:行内容” 格式的对象/Map
javascript
// 歌词解析
function parseLrc(lrc) {
const lines = lrc.split('\n');
const result = {};
const timeRegex = /^\[(\d{2}):(\d{2})\.(\d{2})\]\s*(.+)/;
lines.forEach(line => {
line = line.trim();
const match = line.match(timeRegex);
if (match) {
// Convert time to seconds
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const centiseconds = parseInt(match[3]);
const timeInSeconds = minutes * 60 + seconds + centiseconds / 100;
const text = match[4].trim();
result[timeInSeconds] = text;
}
});
return result;
}
json
{
"26.67": "走在红尘俗世间",
"30.39": "谁的呼唤飘在耳边",
"34.89": "那么熟悉却又遥远",
"39.57": "为什么痴心两处总难相见",
"46.53": "徘徊在起风的午夜",
"50.19": "谁的叹息飘在风间",
"54.69": "那么无奈却又无悔",
"59.13": "多少前世残梦留待今生缘",
"64.02": "就算换了时空变了容颜",
"70.08": "我依然记得你眼里的依恋",
"74.73": "纵然聚散由命也要用心感动天",
"82.8": "就算换了时空变了容颜",
"88.77": "我依然记得你眼里的依恋",
"93.45": "纵然难续前世也要再结今生缘",
"122.34": "走在红尘俗世间",
"126.03": "谁的呼唤飘在耳边",
"130.59": "那么熟悉却又遥远",
"135.18": "为什么痴心两处总难相见",
"142.11": "徘徊在起风的午夜",
"145.86": "谁的叹息飘在风间",
"150.33": "那么无奈却又无悔",
"154.71": "多少前世残梦留待今生缘",
"159.63": "就算换了时空变了容颜",
"165.75": "我依然记得你眼里的依恋",
"170.37": "纵然聚散由命也要用心感动天",
"178.47": "就算换了时空变了容颜",
"184.41": "我依然记得你眼里的依恋",
"189.03": "纵然难续前世也要再结今生缘",
"206.91": "就算换了时空变了容颜",
"212.73": "我依然记得你眼里的依恋",
"217.98": "就算换了时空你变了容颜",
"223.74": "我依然记得你眼里的依恋"
}
然后,我们将歌词逐行对应一个 li
元素显示到面板上。
至于半透明的效果,直接用 mask-image 蒙版来完成,当然你也可以用为元素来套上一层渐变透明效果。
css
mask-image: linear-gradient(to bottom,
rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 1) 50%);
mask-mode: alpha;
最后,最难点是我要将歌曲播放时间点和歌词关联起来。我们可以通过监听 audio 的 timeupdate
事件来达到这一点
javascript
// 监听歌曲播放
const audio = document.querySelector('#audio');
audio.addEventListener('timeupdate', (event) => {
const currentTime = event.target.currentTime;
console.log("🚀 ~ audio.addEventListener ~ currentTime:", currentTime)
// 找到当前时间对应的歌词下一行
const currentInx = sortedTimes.findIndex(time => currentTime < time);
// 移除所有高亮歌词
Array.from(ul.children).forEach(li => {
li.classList.remove('active');
});
if (currentInx > 0) {
// 高亮歌词
const currentLi = ul.children[currentInx - 1];
if (!currentLi.classList.contains('active')) {
currentLi.classList.add('active');
}
// 滚动歌词
ul.style.transform = `translateY(-${(currentInx - 1) * 34}px)`;
}
});
点我查看
timeupdate
事件函数在实际开发建议加上防抖处理
提示
这里我们用 translateY
来做滚动,而不是 scrollTop , 想一想为什么呢?
好了代码大致解释到这里了。你可以进入 CodeSandbox 查看完整代码,快快动手试一试吧!