html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="shortcut icon" href="./assets/favicon.ico" type="image/x-icon"> <link rel="stylesheet" href="./style.css"> <title>歌词滚动效果</title> </head> <body> <div class="contain"> <audio src="https://m704.music.126.net/20240113180245/ce64353d84d93d230abf3ab672bfa732/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/28481681349/59fe/87c8/8ed7/1b985989835af163dd057b1e3cbd74ea.mp3?_authSecret=0000018d022f36cf01dd0aa463681994" controls></audio> <div class="box"> <ul> // 这里等后面使用js动态生成 </ul> </div> </div> <script src="./main.js" type="module"></script> </body> </html>
css
css
* { margin: 0; padding: 0; box-sizing: border-box; list-style: none; } .contain { width: 100vw; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; background: #aa4b6b; /* fallback for old browsers */ background: -webkit-linear-gradient(to right, #3b8d99, #6b6b83, #aa4b6b); /* Chrome 10-25, Safari 5.1-6 */ background: linear-gradient(to right, #3b8d99, #6b6b83, #aa4b6b); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ } audio{ width: 50%; height: 50px; } .box{ width: 550px; height: 420px; text-align: center; color: #f6f6f6; /* 超出隐藏 */ overflow: hidden; } ul { transition: .6s; } li{ height: 30px; line-height: 30px; transition: 0.2s; } .active{ color: rgb(255, 150, 4); transform: scale(1.3); }
js部分
1.导出资源
首先将准备好的歌词字符串进行导出
js
var lrc = `[00:01.06]难念的经 [00:03.95]演唱:周华健 [00:06.78] [00:30.96]笑你我枉花光心计 [00:34.15]爱竞逐镜花那美丽 [00:36.75]怕幸运会转眼远逝 [00:39.32]为贪嗔喜恶怒着迷 [00:41.99]责你我太贪功恋势ƒ [00:44.48]怪大地众生太美丽 ....`; export default lrc
2. 引入资源
在主文件里面进行引入
js
import lrc from "./assets/data.js"; console.log(lrc)
3. 歌词字符串转换为 数组对象的形式
解析歌词 转换为歌词数组对象的形式
因为目前歌词lrc 只是一个字符串对象,里面包含了歌曲时间和对应的歌词,但是在字符串里面,不好操作,我们需要将每一句歌词以及开始的时间放入一个歌词对象里面,然后将每一个歌词对象放入数组里面
js
const parseLrc = () => { // 准备好一个包含歌词对象的数组 const lrcData = []; // 按照"]"字符进行分割 const lines = lrc.split('\n'); // 生成一个数组,用来放置每一句歌词对象(包括时间,和 歌词) // 循环遍历整个歌词数组 for (let i = 0; i < lines.length; i++) { const item = lines[i].split(']') const time = item[0].substring(1); const words = item[1]; const obj = { time: parseTime(time), words } lrcData.push(obj); } return lrcData }
- 观察 歌词字符串
[00:01.06]难念的经
\n[00:03.95]演唱:周华健
我们不难发现,我们首先可以按照\n 进行分割, 得到一个字符串数组
- 然后进行遍历,拿到第二项,再次进行分割 按照
]
进行分割,得到['[00:01.06', '难念的经']
,进而进行针对数组的第一项也就是开始时间 进行substring截取 - 然后创建一个歌词对象,在里面添加属性和值即可,
js
const obj = { time: parseTime(time), words }
- 但是又遇到了问题, 我们上面的时间,如果不进行特殊处理的话,是这样的
00:39.32
, 而我们希望它是39.32
, 我们可以得出一个转换公式第一个项 * 60 + 第二项
转换时间的函数
js
const parseTime = (timeStr) => { // 进行分割 按照:进行分割 const parts = timeStr.split(':'); return Number(parts[0]) * 60 + Number(parts[1]) }
- 最后将处理好的对象 添加到准备好的数组里面即可, 并返回这个数组
4. 绘制页面
创建 元素片段的作用 主要为了优化代码, 提高效率,其实对于这种少数循环插入, 可以不采用.
js
const doms = { audio: document.querySelector('audio'), box: document.querySelector('.box'), ul: document.querySelector('.box ul') }
js
const drawPage = () => { // 创建元素片段 const frag = document.createDocumentFragment(); for (let i = 0; i < resulr.length; i++) { // 创建li标签 const li = document.createElement('li'); // 设置li标签的文本内容 li.textContent = resulr[i].words; frag.appendChild(li); } doms.ul.appendChild(frag); } drawPage()
5. 设置ul元素偏移
这个我们可以看一下图,来更好的方便去理解
这个是最大偏移量, 用来后续做边界判断的
每次更新时间线之后, ul元素的偏移的量:
// 容器高度 var boxclientHeight = doms.box.clientHeight; // 420 // 每个 li 的高度 var liHeight = doms.ul.children[0].clientHeight;// 30 // 最大偏移量 // 最大偏移量 = ul的高度 - 容器高度 // 最大偏移量 = 2160 - 420 = 1740 var maxOffset = doms.ul.clientHeight - boxclientHeight; console.log("最大偏移量", maxOffset); // 高亮歌词 + 设置ul偏移量 const setOffset = () => { let index = findIndex(); // console.log(index); // 30 * 1 + 15 = 45 - 210 = -165 let offset = liHeight * index + liHeight / 2 - 210; console.log(offset); if (offset < 0) { offset = 0 } // 如果超出最大偏移量 设置偏移量为最大偏移量 if (offset > maxOffset) { offset = maxOffset } doms.ul.style.transform = `translateY(-${offset}px)`; console.log(doms.ul.style.transform); // 去掉之前的active样式 let li = doms.ul.querySelector('.active'); if (li) { li.classList.remove('active'); } // 添加active样式 li = doms.ul.children[index]; if (li) { li.classList.add('active'); } }
6. 添加事件监听, 不断执行偏移函数
js
doms.audio.addEventListener('timeupdate', setOffset);
7. 完整js代码
data.js
js
var lrc = `[00:01.06]难念的经 [00:03.95]演唱:周华健 [00:06.78] [00:30.96]笑你我枉花光心计 [00:34.15]爱竞逐镜花那美丽 [00:36.75]怕幸运会转眼远逝 [00:39.32]为贪嗔喜恶怒着迷 [00:41.99]责你我太贪功恋势ƒ [00:44.48]怪大地众生太美丽 [00:47.00]悔旧日太执信约誓 [00:49.66]为悲欢哀怨妒着迷 [00:52.56]啊 舍不得璀灿俗世 [00:57.66]啊 躲不开痴恋的欣慰 [01:02.86]啊 找不到色相代替 [01:08.09]啊 参一生参不透这条难题 [01:13.15]吞风吻雨葬落日未曾彷徨 [01:15.73]欺山赶海践雪径也未绝望 [01:18.23]拈花把酒偏折煞世人情狂 [01:20.90]凭这两眼与百臂或千手不能防 [01:23.76]天阔阔雪漫漫共谁同航 [01:26.09]这沙滚滚水皱皱笑着浪荡 [01:28.68]贪欢一刻偏教那女儿情长埋葬 [01:32.38] [01:34.09]吞风吻雨葬落日未曾彷徨 [01:36.50]欺山赶海践雪径也未绝望 [01:39.07]拈花把酒偏折煞世人情狂 [01:41.69]凭这两眼与百臂或千手不能防 [01:44.68]天阔阔雪漫漫共谁同航 [01:46.93]这沙滚滚水皱皱笑着浪荡 [01:49.54]贪欢一刻偏教那女儿情长埋葬 [01:53.41] [02:15.45]笑你我枉花光心计 [02:18.53]爱竞逐镜花那美丽 [02:21.14]怕幸运会转眼远逝 [02:23.76]为贪嗔喜恶怒着迷 [02:26.43]责你我太贪功恋势 [02:28.98]怪大地众生太美丽 [02:31.60]悔旧日太执信约誓 [02:34.26]为悲欢哀怨妒着迷 [02:36.90]啊 舍不得璀灿俗世 [02:42.04]啊 躲不开痴恋的欣慰 [02:47.34]啊 找不到色相代替 [02:52.52]啊 参一生参不透这条难题 [02:57.47]吞风吻雨葬落日未曾彷徨 [03:00.05]欺山赶海践雪径也未绝望 [03:02.64]拈花把酒偏折煞世人情狂 [03:05.27]凭这两眼与百臂或千手不能防 [03:08.22]天阔阔雪漫漫共谁同航 [03:10.49]这沙滚滚水皱皱笑着浪荡 [03:13.06]贪欢一刻偏教那女儿情长埋葬 [03:18.45]吞风吻雨葬落日未曾彷徨 [03:20.90]欺山赶海践雪径也未绝望 [03:23.54]拈花把酒偏折煞世人情狂 [03:26.21]凭这两眼与百臂或千手不能防 [03:29.07]天阔阔雪漫漫共谁同航 [03:31.32]这沙滚滚水皱皱笑着浪荡 [03:33.92]贪欢一刻偏教那女儿情长埋葬 [03:39.32]吞风吻雨葬落日未曾彷徨 [03:41.84]欺山赶海践雪径也未绝望 [03:44.38]拈花把酒偏折煞世人情狂 [03:47.04]凭这两眼与百臂或千手不能防 [03:49.99]天阔阔雪漫漫共谁同航 [03:52.20]这沙滚滚水皱皱笑着浪荡 [03:54.89]贪欢一刻偏教那女儿情长埋葬 [04:00.28]吞风吻雨葬落日未曾彷徨 [04:02.68]欺山赶海践雪径也未绝望 [04:05.25]拈花把酒偏折煞世人情狂 [04:07.90]凭这两眼与百臂或千手不能防 [04:10.85]天阔阔雪漫漫共谁同航 [04:13.08]这沙滚滚水皱皱笑着浪荡 [04:15.75]贪欢一刻偏教那女儿情长埋葬 [04:19.48]`; export default lrc
main.js
js
import lrc from "./assets/data.js"; /** * 将歌词字符 转换为对象的形式 * obj = {time:开始时间, words: 歌词内容} */ const parseLrc = () => { // 准备好一个包含歌词对象的数组 const lrcData = []; // 按照"]"字符进行分割 const lines = lrc.split('\n'); // console.log(lines); // 生成一个数组,用来放置每一句歌词对象(包括时间,和 歌词) // 循环遍历整个歌词数组 for (let i = 0; i < lines.length; i++) { const item = lines[i].split(']') // console.log(item); const time = item[0].substring(1); const words = item[1].substring(1); const obj = { time: parseTime(time), words } lrcData.push(obj); } return lrcData } // 转换规则 // 00:39.32 ===> 0 + 39 + 0.32 = 39.32 // 01:02.86 ===> 60 + 2 + 0.86 = 62.86 const parseTime = (timeStr) => { // 进行分割 按照:进行分割 const parts = timeStr.split(':'); return Number(parts[0]) * 60 + Number(parts[1]) } const resulr = parseLrc() // console.log(resulr); // 页面绘制 // 获取dom const doms = { audio: document.querySelector('audio'), box: document.querySelector('.box'), ul: document.querySelector('.box ul') } const drawPage = () => { // 创建元素片段 const frag = document.createDocumentFragment(); for (let i = 0; i < resulr.length; i++) { // 创建li标签 const li = document.createElement('li'); // 设置li标签的文本内容 li.textContent = resulr[i].words; frag.appendChild(li); } doms.ul.appendChild(frag); } drawPage() // 找到当前歌词 const findIndex = () => { // 播放器当前时间 const curTime = doms.audio.currentTime; for (let i = 0; i < resulr.length; i++) { if (curTime < resulr[i].time) { return i - 1; } } // 找遍了都没找到(说明播放到最后一句) return lrcData.length - 1; } // 容器高度 var boxclientHeight = doms.box.clientHeight; console.log("boxclientHeight", boxclientHeight); // 每个 li 的高度 var liHeight = doms.ul.children[0].clientHeight; console.log(liHeight); // 30 // 最大偏移量 var maxOffset = doms.ul.clientHeight - boxclientHeight; // 最大偏移量 = ul的高度 - 容器高度 // 最大偏移量 = 2160 - 420 = 1740 console.log("最大偏移量", maxOffset); // 高亮歌词 + 设置ul偏移量 const setOffset = () => { let index = findIndex(); // console.log(index); // 30 * 1 + 15 = 45 - 210 = -165 let offset = liHeight * index + liHeight / 2 - 210; console.log(offset); if (offset < 0) { offset = 0 } if (offset > maxOffset) { offset = maxOffset } doms.ul.style.transform = `translateY(-${offset}px)`; console.log(doms.ul.style.transform); // 去掉之前的active样式 let li = doms.ul.querySelector('.active'); if (li) { li.classList.remove('active'); } // 添加active样式 li = doms.ul.children[index]; if (li) { li.classList.add('active'); } } doms.audio.addEventListener('timeupdate', setOffset);
到此就结束了, 主要难点在于计算ul的偏移量, 和 最大偏移量, 以及边界情况考虑. 不过,只要画好图,一分析,便会清晰明了许多.