本文案例为歌词滚动,随着音乐播放的进度同步滚动,本案例的源码在文章首部即可获取,html、css、js均为单独文件,案例的实现详解可根据需要可在本文后续内容查看
一、源码展示
-
html 文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 设置favicon --> <link rel="shortcut icon" href="./assets/author_favicon.ico" type="image/x-icon"> <!-- 引入css --> <link rel="stylesheet" href="./css/index.css"> <title>歌词滚动效果</title> </head> <body> <audio controls src="./assets/海阔天空.mp3"></audio> <div class="container"> <ul class="lrc-list"></ul> </div> <!-- 引入js --> <script src="./js/mock.js"></script> <script src="./js/index.js"></script> </body> </html>
-
css 文件
* { margin: 0; padding: 0; box-sizing: border-box; } li { list-style: none; } body { width: 100vw; display: flex; justify-content: center; align-items: center; flex-direction: column; background-color: #010409; padding: 50px; color: #666; text-align: center; } audio { width: 600px; } .container { width: 100%; height: 600px; display: flex; flex-direction: column; margin-top: 30px; overflow: hidden; } .container ul { transition: all 0.3s ease-in; } .container li { transition: all 0.3s ease; height: 30px; line-height: 30px; } .container li.active { transform: scale(1.4); color: #fff; }
-
业务逻辑 js 文件
const lrcData = formatLrc(lrc) const audio = document.querySelector('audio') const container = document.querySelector('.container') const ul = document.querySelector('.lrc-list') const containerHeight = container.clientHeight function formatLrc(data) { const lrcs = [] const lrc = data.split('\n').forEach(item => { lrcs.push({ time: formatTime(item), lrc: item.split(']')[1] }) }) return lrcs } function formatTime(str) { const res = str.split(']')[0].substring(1) return res.split(':')[0] * 60 + res.split(':')[1] * 1 } function monitorAudio() { const currentAudioTime = audio.currentTime const index = lrcData.findIndex(item => { return item.time > currentAudioTime }) - 1 return index < 0 ? lrcData.length - 1 : index } function createLrcElement() { const frag = document.createDocumentFragment() lrcData.forEach(item => { const li = document.createElement('li') li.innerText = item.lrc frag.appendChild(li) }) ul.appendChild(frag) } createLrcElement() const liHeight = ul.children[0].clientHeight const maxUpLength = lrcData.length * liHeight - containerHeight function setUpLength() { const index = monitorAudio() let upLength = liHeight * index + liHeight / 2 - containerHeight / 2 if (upLength < 0) { upLength = 0 } if (upLength > maxUpLength) { upLength = maxUpLength } ul.style.transform = `translateY(-${upLength}px)` const lis = [...ul.children] lis.forEach(item => { item.classList.remove('active') }) const li = ul.children[index] if (li) { li.classList.add('active') } } audio.addEventListener('timeupdate', function () { setUpLength() })
-
歌词数据 js 文件
const lrc = `[00:00.000] 作词 : 黄家驹\n[00:01.000] 作曲 : 黄家驹\n[00:02.000] 编曲 : Beyond/梁邦彦\n[00:03.000] 制作人 : Beyond/梁邦彦\n[00:18.466]今天我 寒夜里看雪飘过\n[00:25.110]怀着冷却了的心窝漂远方\n[00:30.950]风雨里追赶 雾里分不清影踪\n[00:37.229]天空海阔你与我\n[00:40.291]可会变 (谁没在变)\n[00:43.440]多少次 迎着冷眼与嘲笑\n[00:50.050]从没有放弃过心中的理想\n[00:55.907]一刹那恍惚 若有所失的感觉\n[01:02.133]不知不觉已变淡\n[01:05.243]心里爱 (谁明白我)\n[01:08.801]原谅我这一生不羁放纵爱自由\n[01:15.799]也会怕有一天会跌倒\n[01:22.008]背弃了理想 谁人都可以\n[01:28.276]哪会怕有一天只你共我\n[01:34.102]\n[01:42.695]今天我 寒夜里看雪飘过\n[01:49.284]怀着冷却了的心窝漂远方\n[01:55.189]风雨里追赶 雾里分不清影踪\n[02:01.405]天空海阔你与我\n[02:04.535]可会变 (谁没在变)\n[02:08.014]原谅我这一生不羁放纵爱自由\n[02:15.040]也会怕有一天会跌倒\n[02:21.279]背弃了理想 谁人都可以\n[02:27.531]哪会怕有一天只你共我\n[02:33.633]\n[03:08.454]仍然自由自我 永远高唱我歌\n[03:15.064]走遍千里\n[03:19.739]原谅我这一生不羁放纵爱自由\n[03:26.734]也会怕有一天会跌倒\n[03:33.005]背弃了理想 谁人都可以\n[03:39.257]哪会怕有一天只你共我\n[03:45.496]背弃了理想 谁人都可以\n[03:51.756]哪会怕有一天只你共我\n[03:57.201]原谅我这一生不羁放纵爱自由\n[04:04.204]也会怕有一天会跌倒\n[04:10.456]背弃了理想 谁人都可以\n[04:16.647]哪会怕有一天只你共我\n[04:22.828]\n[04:31.852] 录音 : Shunichi Yokoi\n[04:40.876] 混音 : Shunichi Yokoi\n[04:49.900] 录音室 : Greenbird St./Tokyu Fun St./West Side St.(Tokyo/From Jan/to Apr./1993)\n[04:58.924] 母带工程师 : Setsu Hisai at Tokyu Fun St.\n[05:07.948] 弦乐 : 桑野圣乐团 (Kuwano Strings)\n[05:16.972] OP : Amuse Inc. & Fun House Inc.\n[05:25.996] SP : Amuse H.K. Ltd.`
二、音乐文件和ico图标生成
三、业务逻辑JS实现讲解
-
总体实现思路如下,每个 li 标签都有固定的高度,只是超出容器隐藏,那么 li 标签的父盒子 ul 就会被撑高,我们只需要控制 ul 的上下移动距离即可,通过移动的距离就可以达到歌词滚动效果,同时让当前的歌词高亮
-
格式化歌词数据函数,比较简单就不在做过多的赘述了,只是需要形成
[{time: xx, lrc:xx},...]
的数据类型即可,代码如下// 格式化歌词方法-->字符串转为数组 function formatLrc(data) { // 定义歌词总数组 const lrcs = [] // 使用换行符分割成数字 const lrc = data.split('\n').forEach(item => { lrcs.push({ time: formatTime(item), lrc: item.split(']')[1] }) }) return lrcs } // 将时间转为数值 function formatTime(str) { const res = str.split(']')[0].substring(1) return res.split(':')[0] * 60 + res.split(':')[1] * 1 }
-
将歌词的时间转为秒数后就可以通过获取当前音乐进度的时间进行对比,只要当前音乐播放的进度时间小于我们数组中音乐时间的某一项,就可以停止查找,比如现在音乐播放到第40秒,那么假设第三局歌词是37秒开始展示,第四句歌词是46秒展示,是不是就代表目前播放的是第三局歌词呢,那就可以得到一个公式,返回符合条件的下标 -1,就可得到当前播放的歌词下标,就可以通过下标去我们格式化好的数据里面取出对应的内容,当然如果播放到了最后,也就不存在歌词数组中某一项大于当前音乐播放时间了,这时候 findIndex 返回的索引就为 -1,因此检测到下标小于 0,就表示播放完成,定歌最后一句歌词即可,函数方法实现如下:
// 监听音乐播放时间,根据进度让对应时间的歌词的下标 function monitorAudio() { // 使用内置api currentTime 获取当前播放的时间 const currentAudioTime = audio.currentTime // 如果当前播放时间小于某一项的时间,就返回他前面一项的数据 const index = lrcData.findIndex(item => { return item.time > currentAudioTime }) - 1 // 若索引小于0表示歌曲播放结束了 return index < 0 ? lrcData.length - 1 : index }
-
那么有了歌词的信息,就应该渲染 dom,实现展示了,根据歌词数组的长度渲染对应数量的 li 元素即可,一个 li 元素对应一句歌词,这里我们可以直接使用循环创建 li,但是这样比较耗费资源,虽然本案例中创建的次数不多,但是也可以进行优化,可以创建文档片段,先将所有的 li 加入到文本片段当中,结束后在将文本片段插入到 ul 下,即我们需要上下移动的 ul 容器,代码如下:
// 创建渲染歌词信息元素方法 function createLrcElement() { // 创建文档片段,将多次修改dom改为一次修改,先将所有的li给文档片段,在将文档片段给ul const frag = document.createDocumentFragment() lrcData.forEach(item => { // 创建li元素 const li = document.createElement('li') // 给每个li元素赋值对应的歌词显示 li.innerText = item.lrc // 将li添加到ul,显示在页面上 frag.appendChild(li) }) ul.appendChild(frag) } createLrcElement()
-
此时我们可以决定 ul 具体的上下移动距离是多少了,此方法均为计算,大家可以自行通过注释查看,代码如下:
// 接收格式化后的歌词数据 const lrcData = formatLrc(lrc) // 获取播放器dom const audio = document.querySelector('audio') // 获取容器 const container = document.querySelector('.container') // 获取ul const ul = document.querySelector('.lrc-list') // 获取容器的高度 const containerHeight = container.clientHeight // 获取承载每行歌词元素li的高度 const liHeight = ul.children[0].clientHeight // 定义向上最大的移动距离 const maxUpLength = lrcData.length * liHeight - containerHeight // 设置歌词向上滚动的距离 function setUpLength() { // 获取当前歌词显示的索引值 const index = monitorAudio() // ul向上的偏移量等于单个li的高度*当前li的索引值,因为歌词高亮居中显示,额外加上一个li的高度的一半,再减去外层容器高度的一半 let upLength = liHeight * index + liHeight / 2 - containerHeight / 2 // 同时如果在开始的前几句歌词因为需要局中显示,会导致ul的顶部会在容器的中,移动的距离是负数,所以在移动距离小于0时,移动距离强制为0 if (upLength < 0) { upLength = 0 } // 如果希望最后一句歌词时,ul的底部与容器的底部重合,可以设置最大移动距离,超出这个值就不在继续增加,最大移动距离为ul的总高度也就是所有li的高度累加减去容器的高度 if (upLength > maxUpLength) { upLength = maxUpLength } // 开始移动ul ul.style.transform = `translateY(-${upLength}px)` // 获取所有的li const lis = [...ul.children] // 使用排他思想,先清楚其余里的高亮-也可以使用querySelector获取第一个类名,需要保持此类名的唯一性 lis.forEach(item => { item.classList.remove('active') }) // 同时让当前的歌词高亮,但是如果li无值就不显示 const li = ul.children[index] if (li) { li.classList.add('active') } }
-
移动的方法定义好之后,如何监听使用呢,我们可以使用 audio 标签自带的事件
timeupdate
监听时间变化触发事件,代码如下:// 使用audio内置的事件 timeupdate,时间变化触发事件 audio.addEventListener('timeupdate', function () { setUpLength() })
四、总结
上述就是本案例的全部内容了,如果大家想要更换歌曲的时候需要 音乐文件 或者 歌词数据 素材可以选择网易云的 API,网易云API文档地址:网易云音乐 NodeJS 版 API