近期浏览了一堂公开课讲的《滚动歌词》的经典动效案例,这里我将通过文字按我的开发思路和顺序来进行讲述和实现。
- 实现静态页面
- 验证滚动方案
- 解析歌词数据
- 插入歌词元素
- 实现滚动方案
- 偏移公式讲解
实现静态页面
首先在html
的body
部分增加一个container
,用来包裹由每一行歌词文本组成的ul
无序列表:
<body>
<div class="container">
<ul>
<li>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Dicta, doloribus.</li>
<li>Nulla, porro nisi quisquam sunt laudantium fugiat odit quas ipsa!</li>
<li>Debitis, quibusdam in illo perferendis voluptates beatae ratione? Beatae, nisi?</li>
</ul>
</div>
</body>
接着对页面做一些样式调整:
- 容器样式调整:重置
margin
和padding
,设置body
背景色、文本颜色及对齐方式,设置.container
默认高度并禁止内容溢出(歌词的无序列表要在此容器中进行滚动展示);
* {
margin: 0;
padding: 0;
}
body {
background: #000;
color: #666;
text-align: center;
}
.container {
height: 300px;
overflow: hidden;
}
- 歌词无序列表初始化位置:默认将歌词无序列表的位置移动到
.container
容器垂直居中,也就是容器高度/2 - 每行歌词高度/2 = 135px
的位置,在进行移动时优先使用transform
进行非几何属性的变化,避免回流(reflow)频繁发生;
.container ul {
transform: translateY(135px);
}
- 每行歌词的样式调整:设置每行歌词的高度,并给定一个与高度一致的
line-height
,让歌词文本居中显示;还会增加一个active
样式类,用来高亮并放大显示当前播放的歌词;
.container li {
height: 30px;
line-height: 30px;
}
.container li.active {
color: #ccc;
transform: scale(1.2);
}
增加过渡效果:
歌词无序列表的滚动和当前播放歌词的放大目前都是生硬的,需要为它们增加一些过渡效果,这样会顺滑一些。这里会用到 transition
属性,可以指定要生效的 property name
和 duration
。
- 歌词无序列表滚动:
.container ul {
transform: translateY(135px);
transition: transform 0.5s;
}
- 当前播放歌词放大:
.container li {
height: 30px;
line-height: 30px;
transition: transform 0.5s;
}
注:这部分内容仅为静态内容,页面样式及动效通过控制台修改参数来进行简单验证。
验证滚动方案
音频播放需要用到 audio
标签,所以首先要在页面中插入一个 audio
标签;
<body>
<audio controls src="./assets/2177806392.mp3"></audio>
</body>
audio
在对音频进行播放期间当 currentTime
更新时会触发 timeupdate
事件,也就是说,我们可以通过监听 timeupdate
事件来获取当前播放的位置(时间)currentTime
(单位:秒)。
const audio = document.querySelector("audio");
audio.addEventListener("timeupdate", () =>
console.log(`当前播放到 ${audio.currentTime} 秒`)
);
播放的时间搞定后,就需要与歌词文件进行匹配,歌词文件选择包含时间的 lrc
文件,在 lrc
文件中每一行为一组包含了时间和歌词的数据,时间是由 [分:秒]
组成的,需要将时间进行一定的转换后在与 audo
的 currentTime
进行匹配;
function paresTime() {
const times = timeStr && timeStr.split(":");
if (times && times.length === 2) {
const minute = times[0];
const second = times[1];
return +minute * 60 + +second;
}
return 0;
}
解析歌词数据
通过 fetch
函数加载Lrc
歌词文件并将歌词数据对象化处理:会通过 split
、slice
及 map
进行处理;
async function getLrcData() {
const data = await fetch("./assets/罗刹海市.lrc").then((response) =>
response.text()
);
return data.split("\n").map((line) => {
return {
time: paresTime(line.split("]")[0].slice(1)),
words: line.split("]")[1],
};
});
}
编写一个自执行函数来运行获取歌词;
(async ()=>{
const data = await getLrcData();
console.log(data);
})()
插入歌词元素
创建一个 DocumentFragment
,接收遍历歌词数据时创建了 li
元素,在结束遍历后统一添加到 ul
无序列表中;
function genLrcFragment(data) {
const fragment = document.createDocumentFragment();
data.forEach(line => {
const li = document.createElement('li');
li.textContent = line.words;
fragment.appendChild(li);
});
return fragment;
}
const fragment = genLrcFragment(data);
document.querySelector('.container ul').appendChild(fragment);
实现滚动方案
- 确定当前播放音乐对应歌词在歌词数据中的下标位置:如当前播放时间为
14
秒,那么当前高亮的应该就是14.36
秒前的一句歌词;但当前播放时间大于04:32.3
秒(完整音乐播放到05:32
秒)时,始终返回歌词数据的最后一个下标位置;
const index = data.findIndex((line) => currentTime < line.time);
const highlightIndex = index != -1 ? index - 1 : data.length - 1;
- 获取歌词需要偏移(移动)的距离:某下标[x]的歌词位置 = 容器高度/2 – 每行歌词高度/2 – 每行歌词高度*[x];
let containerHeight = 0;
let liHeight = 0;
let uldom = null;
if (!containerHeight) {
containerHeight = document.querySelector('.container').clientHeight;
}
if (!liHeight) {
liHeight = document.querySelector('.container ul').children[0].clientHeight;
}
// 300/2 - 30*1 - 30/2 = 105px
const offset = containerHeight / 2 - liHeight * highlightIndex - liHeight / 2;
if (!uldom) {
uldom = document.querySelector('.container ul');
}
uldom.style.transform = `translateY(${offset}px)`;
- 处理高亮歌词:每次高亮新的歌词之前要移除已高亮部分;
function highlight(data, currentTime) {
// 移除高亮
const active = document.querySelector(".active");
active && active.classList.remove("active");
const index = data.findIndex((line) => currentTime < line.time);
const highlightIndex = index != -1 ? index - 1 : data.length - 1;
if (!containerHeight) {
containerHeight = document.querySelector('.container').clientHeight;
}
if (!liHeight) {
liHeight = document.querySelector('.container ul').children[0].clientHeight;
}
const offset = containerHeight / 2 - liHeight * highlightIndex - liHeight / 2;
if (!uldom) {
uldom = document.querySelector('.container ul');
}
// 添加新歌词的高亮
uldom.children[highlightIndex].classList.add("active");
uldom.style.transform = `translateY(${offset}px)`;
}
偏移公式讲解
公式:某下标[x]的歌词位置 = 容器高度/2 – 每行歌词高度/2 – 每行歌词高度*[x];
我们默认的容器高度为 300px
,一半的容器高度就是 150px
,ul
直接偏移 容器高度/2
后其实并非容器的正中间,而是多偏移了 15px
,也就是每行歌词一半的距离,所以需要减去 每行高度/2
的一个距离,剩下的在图中也可以看的出来,下标为 1
的时候 每行歌词高度*1
,下标为 2
的时候 每行歌词高度*2
。最后实际的偏移高度就如图和公式所示进行相减获得。
总结
在实现案例之前,首先要做的就是熟悉歌词数据,搞清楚歌词数据中包含有哪些内容,同时要结合 audio
标签提供的事件进行监听并获取到实时的播放时间,能相互匹配确认可行后才能着手开发。
如果看完觉得有收获,欢迎点赞、评论、分享支持一下。你的支持和肯定,是我坚持写作的动力~