先上图:
原理
一个audio标签,一个歌曲文件和一个歌词文件,然后就是JS控制歌词滚动逻辑和界面布局
听起来十分简单,但是,真正要不借助任何框架做出来才知道还是挺考验基础功的
1、页面布局
要有一个audio音乐播放标签,和一个歌词容器,然后引入样式表、歌词js文件和主逻辑js文件
<div class="app">
<!-- 音乐播放器 -->
<audio src="./src/雲翼星辰 - 黑暗森林.mp3" controls></audio>
<!-- 歌词 -->
<div class="container">
<ul class="lrc_list"></ul>
</div>
</div>
<script src="./lrc.js"></script>
<script src="./index.js"></script>
2、拿到歌曲和歌词
随便下载一个歌曲文件和一个歌词文件,歌词文件格式为 .lrc,打开之后,复制到 lrc.js 文件中,然后定义一个全局变量var lrc = 'xxxx',方便我们使用
歌词格式,就是一段字符串,但是,字符串不方便操作,因此需要转换为js对象:
function parseLrc() {
var lines = lrc.split('\n');
var result = []; // 歌词对象数组
for (var i = 0; i < lines.length; i++) {
var str = lines[i];
var parts = str.split(']');
var timeStr = parts[0].substring(1);
var parts = timeStr.split(':');
var obj = {
time: +parts[0] * 60 + +parts[1],
words: parts[1],
};
result.push(obj);
}
return result;
}
得到的是一个对象数组,有时间和对应的文字:
3、获取对应的DOM
为方便管理,把dom都封装到一个对象中
//歌词对象
var lrcData = parseLrc();
// 获取需要的 dom
var doms = {
audio: document.querySelector('audio'),
ul: document.querySelector('.lrc_list'),
container: document.querySelector('.container'),
};
4、获取播放器时间对应的歌词
function findIndex() {
// 播放器当前时间
var curTime = doms.audio.currentTime;
for (var i = 0; i < lrcData.length; i++) {
if (curTime < lrcData[i].time) {
return i - 1;
}
}
// 找遍了都没找到(说明播放到最后一句)
return lrcData.length - 1;
}
获取当前播放器的时间,然后返回对应时间的歌词
5、创建歌词元素 li
使用js根据歌词对象,为每句歌词动态地创建li元素
function createLrcElements() {
var frag = document.createDocumentFragment(); // 文档片段
for (var i = 0; i < lrcData.length; i++) {
var li = document.createElement('li');
li.textContent = lrcData[i].words;
frag.appendChild(li); // 改动了 dom 树
}
doms.ul.appendChild(frag);
}
createLrcElements();
6、计算歌词偏移
播放的歌词会添加一个特殊样式,放大且字体为白色,需要动态添加,最后添加音乐播放标签的播放时间事件,根据播放时间,计算歌词元素滚动的偏移量
// 容器高度
var containerHeight = doms.container.clientHeight;
// 每个 li 的高度
var liHeight = doms.ul.children[0].clientHeight;
// 最大偏移量
var maxOffset = doms.ul.clientHeight - containerHeight;
// 设置偏移
function setOffset() {
var index = findIndex();
var offset = liHeight * index + liHeight / 2 - containerHeight / 2;
if (offset < 0) {
offset = 0;
}
if (offset > maxOffset) {
offset = maxOffset;
}
doms.ul.style.transform = `translateY(-${offset}px)`;
// 去掉之前的 active 样式
var li = doms.ul.querySelector('.active');
if (li) {
li.classList.remove('active');
}
li = doms.ul.children[index];
if (li) {
li.classList.add('active');
}
}
doms.audio.addEventListener('timeupdate', setOffset);