对于很多音乐APP,都有这么一个功能,就是根据歌曲的进度来控制对应的歌词滚动,如下图所示:
大概这样的效果,我此次是使用原生的HTML+CSS+JS来实现的,以下是具体的实现过程。
1. 数据获取与处理
对于数据来源,这里由于只有前端展示,所以我们直接使用死数据,杰伦的《我不配》,数据如下(音频可以找我要):
歌词数据如下:
var lrc = `[00:00.06]︿☆我不配☆︿
[00:00.75]
[00:01.11]演唱:周杰伦
[00:02.62]
[00:03.35]︿☆歌词制作:ikun
[00:06.13]→QQ:2682548155←
[00:09.30]www.90lrc.cn ★【歌词网】
[00:11.09]
[00:18.40]这街上太拥挤 太多人有秘密
[00:22.66]玻璃上有雾气 在被隐藏起过去
[00:27.10]你脸上的情绪 在还原那场雨
[00:31.61]这巷弄太过弯曲 走不回故事里
[00:36.00]
[00:36.10]这日子不再绿 又斑驳了几句
[00:40.49]剩下搬空回忆的我在大房子里
[00:44.92]电影院的座椅 隔遥远的距离
[00:49.24]感情没有对手戏 你跟自己下棋
[00:53.70]
[00:53.80]还来不及 仔仔细细写下你的关于
[01:02.65]描述我如何爱你 你却微笑的离我而去
[01:10.90]
[01:11.50]这感觉 已经不对 我努力在挽回
[01:15.90]一些些 应该体贴的感觉 我没给
[01:20.32]你嘟嘴 许的愿望很卑微 在妥协
[01:24.50]是我忽略 你不过要人陪
[01:29.10]
[01:29.19]这感觉 已经不对 我最后才了解
[01:33.57]一页页 不忍翻阅的情节 你好累
[01:38.03]你默背 为我掉过几次泪 多憔悴
[01:42.30]而我心碎 你受罪你的美 我不配
[01:49.58]
[02:04.98]这街上太拥挤 太多人有秘密
[02:09.37]玻璃上有雾气 在被隐藏起过去
[02:13.80]你脸上的情绪 在还原那场雨
[02:18.25]这巷弄太过弯曲 走不回故事里
[02:22.61]
[02:22.82]这日子不再绿 又斑驳了几句
[02:27.26]剩下搬空回忆的我在大房子里
[02:31.58]电影院的座椅 隔遥远的距离
[02:35.99]感情没有对手戏 你跟自己下棋
[02:40.24]
[02:40.26]还来不及 仔仔细细写下你的关于
[02:49.04]描述我如何爱你 你却微笑的离我而去
[02:57.42]
[02:58.20]这感觉 已经不对 我努力在挽回
[03:02.56]一些些 应该体贴的感觉 我没给
[03:06.98]你嘟嘴 许的愿望很卑微 在妥协
[03:11.10]是我忽略 你不过要人陪
[03:15.50]
[03:15.78]这感觉 已经不对 我最后才了解
[03:20.20]一页页 不忍翻阅的情节 你好累
[03:24.66]你默背 为我掉过几次泪 多憔悴
[03:28.98]而我心碎 你受罪你的美 我不配
[03:36.12]
[03:47.30]这感觉 已经不对 我努力在挽回
[03:51.38]一些些 应该体贴的感觉 我没给
[03:55.79]你嘟嘴 许的愿望很卑微 在妥协
[04:00.00]是我忽略 你不过要人陪
[04:04.54]
[04:04.64]这感觉 已经不对 我最后才了解
[04:09.03]一页页 不忍翻阅的情节 你好累
[04:13.64]你默背 为我掉过几次泪 多憔悴
[04:17.95]而我心碎 你受罪你的美 我不配
[04:25.70]`
问题来了,这个数据是一条又丑又长的字符串,我们需要把他解析成对象才好处理啊,因此,第一件事应该是写解析函数:
const parseTime = (arr) => { //将时间解析成秒s
let times = arr.split(":");
let seconds = parseFloat(times[0])*60+parseFloat(times[1]);
return seconds;
}
export const parseLrc = () => { //解析字符串
let result = [];
let lines = lrc.split("\n");
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let arrs = line.split("]");
let obj = {
time: parseTime(arrs[0].substring(1)),
text: arrs[1]
}
result.push(obj);
}
return result;
}
2. 页面设计
之后我们把HTML页面和CSS样式大概写好,这里比较简单,直接写上:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>歌曲播放器</title>
<link rel="stylesheet" href="css/index.css" />
</head>
<body>
<audio src="asserts/music.mp3" controls></audio>
<div class="container">
<ul class="data-list">
</ul>
</div>
</body>
<script type="module" src="js/index.js"></script>
</html>
CSS样式如下:
*{
margin: 0;
padding: 0;
}
body{
background-color: #000;
color: #666;
text-align: center;
}
audio {
width: 450px;
margin: 30px 0;
}
.container{
height: 500px;
overflow: hidden;
}
.container::-webkit-scrollbar{
display: none;
}
.container ul{
list-style: none;
transition: 0.6s;
/*transform: translateY(-20px);*/
}
.container ul li{
height: 30px;
line-height: 30px;
font-size: 18px;
transition: 0.6s;
}
.container ul li.active{
color: #fff;
transform: scale(1.2);
}
3. 歌词滚动效果实现
对于歌词滚动效果,我们具体分析一下,无非就是歌词整体向上移,时间点对应的词高亮一下,对于高亮效果,直接使用.active的CSS属性来实现,具体就是将字体放大,颜色变为亮白色:
.container ul li.active{
color: #fff;
transform: scale(1.2);
}
对于移动,我们可以根据audio的进度来移动这个music-list容器,可以使用margin-top或者transform:translateY()来移动,而具体的移动高度可以参考下图
自此,我们可以实现这个效果了:
// 移动...
let currentIndex = 0;
const move = () => {
currentIndex = getMusicIndex(); //当前高亮的歌词下标
let containerHeight = domData.container.clientHeight; //Container高度
let liHeight = domData.ul.children[0].clientHeight; // 每个li标签的高度
let movePx = liHeight * currentIndex + liHeight/2 - containerHeight/2; //需要移动的
let maxMove = domData.ul.clientHeight - containerHeight/2;
// 范围判断
if(movePx < 0){
movePx = 0;
}
if(movePx > maxMove){
movePx = maxMove;
}
// 取消前面的高亮
let activeLi = domData.ul.querySelector('.active');
if(activeLi){
activeLi.classList.remove("active");
}
// 实现高亮
let currentLi = domData.ul.children[currentIndex];
if(currentLi){
currentLi.classList.add('active');
}
// 移动
domData.ul.style.transform = `translateY(-${movePx}px)`;
}
domData.audio.addEventListener('timeupdate',move);
4. index.js
import {parseLrc} from "./data.js";
let domData = {
ul: document.querySelector('.container ul'),
audio: document.querySelector('audio'),
container: document.querySelector('.container'),
} //dom数据
let musicObj = parseLrc(); // 音乐数据
const getAudioTime = () => {
let result = domData.audio.currentTime;
return result;
}
const addMusic = () => {
let documentFragment = document.createDocumentFragment();
for(let i = 0; i < musicObj.length; i++){
let li = document.createElement('li');
li.textContent = musicObj[i].text;
documentFragment.appendChild(li);
}
domData.ul.appendChild(documentFragment);
}
// 根据时间来获取当前需要显示的条数
const getMusicIndex = () => {
let time = getAudioTime();
for(let i = 0;i < musicObj.length; i++) {
let musicTime = musicObj[i].time;
if(time < musicTime){
return i-1;
}
}
return musicObj.length - 1;
}
// 移动...
let currentIndex = 0;
const move = () => {
currentIndex = getMusicIndex(); //当前高亮的歌词下标
let containerHeight = domData.container.clientHeight; //Container高度
let liHeight = domData.ul.children[0].clientHeight; // 每个li标签的高度
let movePx = liHeight * currentIndex + liHeight/2 - containerHeight/2; //需要移动的
let maxMove = domData.ul.clientHeight - containerHeight/2;
// 范围判断
if(movePx < 0){
movePx = 0;
}
if(movePx > maxMove){
movePx = maxMove;
}
// 取消前面的高亮
let activeLi = domData.ul.querySelector('.active');
if(activeLi){
activeLi.classList.remove("active");
}
// 实现高亮
let currentLi = domData.ul.children[currentIndex];
if(currentLi){
currentLi.classList.add('active');
}
// 移动
domData.ul.style.transform = `translateY(-${movePx}px)`;
}
domData.audio.addEventListener('timeupdate',move);
const init = () => {
//获取页面歌词
// 插入歌词
addMusic();
console.log(musicObj)
// 根据时间来移动
}
init(); //入口函数