歌词滚动效果
主要功能:歌词与进度联动
- 歌词随进度滚动
- 点击歌词跳转播放
- 点击进度条歌词滚动到对应位置
效果
思路整理
- 功能模块
- 歌词展示
- 歌曲 mp3 文件、歌词数据(网上搜索获取)
- 播放器
- HTML5 audio 标签
- 实现方法
根据歌词文件的数据动态生成歌词
将歌词数据的格式转化处理
- js
parseLrc(data); // '[00:24.50]听说白雪公主在逃跑' => {time: '00:24.50', lrc: '听说白雪公主在逃跑'}
parseLrc(data); // '[00:24.50]听说白雪公主在逃跑' => {time: '00:24.50', lrc: '听说白雪公主在逃跑'}
- js
parseTime(time); //'01:24.50' => 84.50
parseTime(time); //'01:24.50' => 84.50
使用 li 创建歌词, 并将time转化好,用dataset保存至标签属性,之后方便获取
多个元素一起创建,使用 createElementFragment,效率相对高些
给歌词和播放器注册监听事件
- 点击歌词跳转高亮并滚动,并进入播放状态
- 点击播放器进度,找出对应歌词,并滚动至对应位置
目录
base
📦lyrics-roll
┣ 📜index.html
┣ 📜index.css
┣ 📜index.js
┣ 📜lrc.js
┗ 📜 童话镇.mp3
📦lyrics-roll
┣ 📜index.html
┣ 📜index.css
┣ 📜index.js
┣ 📜lrc.js
┗ 📜 童话镇.mp3
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./index.css">
<title>播放器</title>
</head>
<body>
<div id="container">
<main>
<ul class="lrc-wrap"></ul>
</main>
<div class="control-wrap">
<audio src="./童话镇.mp3" controls controlsList="nodownload noplaybackrate"></audio>
</div>
</div>
<script src="./lrc.js"></script>
<script src="./index.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./index.css">
<title>播放器</title>
</head>
<body>
<div id="container">
<main>
<ul class="lrc-wrap"></ul>
</main>
<div class="control-wrap">
<audio src="./童话镇.mp3" controls controlsList="nodownload noplaybackrate"></audio>
</div>
</div>
<script src="./lrc.js"></script>
<script src="./index.js"></script>
</body>
</html>
css
* {
margin: 0;
padding: 0;
}
html,
body,
#container {
width: 100%;
height: 100vh;
}
#container {
position: relative;
display: flex;
flex-direction: column;
background: #282828;
}
main {
position: relative;
flex: 1;
overflow: hidden;
}
main::after,
main::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50%;
background-image: linear-gradient(to top, #0000, #000c);
pointer-events: none;
z-index: 10;
}
main::before {
top: unset;
bottom: 0;
transform: rotateX(180deg);
}
.lrc-wrap {
box-sizing: border-box;
transform: translateY(0px);
position: relative;
transition: transform 0.5s ease-out;
padding-top: calc(50vh - 18px);
padding-bottom: calc(50vh - 18px);
height: 100vh;
overflow: auto;
}
.lrc-wrap > li {
text-align: center;
list-style: none;
font-size: 1.2em;
line-height: 2.5;
transition: 0.3s ease-in;
color: rgba(245, 245, 245, 0.4);
cursor: pointer;
user-select: none;
height: 36px;
}
.lrc-wrap > li.active {
transform: scale(1.5);
color: #f5f5f5;
}
.control-wrap {
flex: 0 0 50px;
background-color: #f1f3f4;
}
audio {
width: 100%;
}
* {
margin: 0;
padding: 0;
}
html,
body,
#container {
width: 100%;
height: 100vh;
}
#container {
position: relative;
display: flex;
flex-direction: column;
background: #282828;
}
main {
position: relative;
flex: 1;
overflow: hidden;
}
main::after,
main::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50%;
background-image: linear-gradient(to top, #0000, #000c);
pointer-events: none;
z-index: 10;
}
main::before {
top: unset;
bottom: 0;
transform: rotateX(180deg);
}
.lrc-wrap {
box-sizing: border-box;
transform: translateY(0px);
position: relative;
transition: transform 0.5s ease-out;
padding-top: calc(50vh - 18px);
padding-bottom: calc(50vh - 18px);
height: 100vh;
overflow: auto;
}
.lrc-wrap > li {
text-align: center;
list-style: none;
font-size: 1.2em;
line-height: 2.5;
transition: 0.3s ease-in;
color: rgba(245, 245, 245, 0.4);
cursor: pointer;
user-select: none;
height: 36px;
}
.lrc-wrap > li.active {
transform: scale(1.5);
color: #f5f5f5;
}
.control-wrap {
flex: 0 0 50px;
background-color: #f1f3f4;
}
audio {
width: 100%;
}
js
/**
* @description: 解析歌词文件
* @param {*} data
* @return {*}
*/
function parseLrc(data) {
return data
.split("\n")
.map((str) => {
var res = str.match(/^\[(?<time>.*)\](?<lrc>.*)?$/);
return res ? { time: res.groups.time, lrc: res.groups.lrc || "" } : undefined;
})
.filter(Boolean);
}
/**
* @description: 处理时间格式
* @return {*} parseTime("02:49.23") => 169.23
*/
function parseTime(timeStr) {
var arr = timeStr.split(":").map((_str) => parseFloat(_str));
return +(arr[0] * 60 + arr[1]).toFixed(2);
}
function setActive(el) {
if (!el || el.tagName.toLocaleLowerCase() != "li") return;
var halfHeight = main.offsetHeight / 2;
var offsetHeight = el.offsetTop;
// lrcWrap.style.transform = `translateY(${-offsetHeight}px)`;
lrcWrap.scroll({ top: offsetHeight - halfHeight });
liList.forEach((li) => li.el.classList.remove("active"));
el.classList.add("active");
currentActiveEl = el;
}
/**
* @description: 创建每行歌词
* @param {*} data 处理好的歌词数据
* @return {*} [{el, time }]
*/
function createElement(data) {
return data.map((row) => {
const li = document.createElement("li");
const time = parseTime(row.time);
li.innerText = row.lrc;
li.dataset.time = time;
return { el: li, time };
});
}
/**
* @description: 生成歌词
* @param {*} data
* @return {*}
*/
function appendLrc(data) {
const fragment = document.createDocumentFragment();
data.forEach((item) => fragment.appendChild(item.el));
lrcWrap.appendChild(fragment);
}
/** @type {HTMLDivElement} */
const lrcWrap = document.querySelector(".lrc-wrap");
var liList = createElement(parseLrc(lrc));
/** @type {HTMLAudioElement} */
var audio = document.querySelector("audio");
var main = document.querySelector("main");
var currentActiveEl = null;
// 给歌词注册监听事件
lrcWrap.addEventListener("click", (e) => {
setActive(e.target);
const time = e.target.dataset.time;
if (!time) return;
// 设置播放位置
audio.currentTime = +time;
// 开始播放
if (audio.paused) audio.play();
});
// 给播放器注册监听事件
audio.addEventListener("loadedmetadata", () => {
console.log("loadedmetadata");
});
audio.addEventListener("timeupdate", () => {
const currentTime = audio.currentTime;
const el = findLrcEl(liList, currentTime);
if (el && currentActiveEl != el) setActive(el);
});
/**
* @description: 查找对应歌词li
* @param {*} liList
* @param {*} currentTime
* @return {*} el
*/
function findLrcEl(liList, currentTime) {
let idx = liList.findIndex((li) => li.time > currentTime);
if (idx) {
return liList[idx - 1].el;
}
}
appendLrc(liList);
/**
* @description: 解析歌词文件
* @param {*} data
* @return {*}
*/
function parseLrc(data) {
return data
.split("\n")
.map((str) => {
var res = str.match(/^\[(?<time>.*)\](?<lrc>.*)?$/);
return res ? { time: res.groups.time, lrc: res.groups.lrc || "" } : undefined;
})
.filter(Boolean);
}
/**
* @description: 处理时间格式
* @return {*} parseTime("02:49.23") => 169.23
*/
function parseTime(timeStr) {
var arr = timeStr.split(":").map((_str) => parseFloat(_str));
return +(arr[0] * 60 + arr[1]).toFixed(2);
}
function setActive(el) {
if (!el || el.tagName.toLocaleLowerCase() != "li") return;
var halfHeight = main.offsetHeight / 2;
var offsetHeight = el.offsetTop;
// lrcWrap.style.transform = `translateY(${-offsetHeight}px)`;
lrcWrap.scroll({ top: offsetHeight - halfHeight });
liList.forEach((li) => li.el.classList.remove("active"));
el.classList.add("active");
currentActiveEl = el;
}
/**
* @description: 创建每行歌词
* @param {*} data 处理好的歌词数据
* @return {*} [{el, time }]
*/
function createElement(data) {
return data.map((row) => {
const li = document.createElement("li");
const time = parseTime(row.time);
li.innerText = row.lrc;
li.dataset.time = time;
return { el: li, time };
});
}
/**
* @description: 生成歌词
* @param {*} data
* @return {*}
*/
function appendLrc(data) {
const fragment = document.createDocumentFragment();
data.forEach((item) => fragment.appendChild(item.el));
lrcWrap.appendChild(fragment);
}
/** @type {HTMLDivElement} */
const lrcWrap = document.querySelector(".lrc-wrap");
var liList = createElement(parseLrc(lrc));
/** @type {HTMLAudioElement} */
var audio = document.querySelector("audio");
var main = document.querySelector("main");
var currentActiveEl = null;
// 给歌词注册监听事件
lrcWrap.addEventListener("click", (e) => {
setActive(e.target);
const time = e.target.dataset.time;
if (!time) return;
// 设置播放位置
audio.currentTime = +time;
// 开始播放
if (audio.paused) audio.play();
});
// 给播放器注册监听事件
audio.addEventListener("loadedmetadata", () => {
console.log("loadedmetadata");
});
audio.addEventListener("timeupdate", () => {
const currentTime = audio.currentTime;
const el = findLrcEl(liList, currentTime);
if (el && currentActiveEl != el) setActive(el);
});
/**
* @description: 查找对应歌词li
* @param {*} liList
* @param {*} currentTime
* @return {*} el
*/
function findLrcEl(liList, currentTime) {
let idx = liList.findIndex((li) => li.time > currentTime);
if (idx) {
return liList[idx - 1].el;
}
}
appendLrc(liList);
js
const lrc = `[00:01.78]童话镇 - 许娜
[00:03.14]词:竹君
[00:05.23]曲:暗杠
[00:06.92]录制:文克津
[00:24.50]听说白雪公主在逃跑
[00:26.74]小红帽在担心大灰狼
[00:29.91]听说疯帽喜欢爱丽丝
[00:33.39]丑小鸭会变成白天鹅
[00:36.89]听说彼得潘总长不大
[00:40.24]杰克他有竖琴和魔法
[00:43.75]听说森林里有糖果屋
[00:47.23]灰姑娘丢了心爱的玻璃鞋
[00:51.13]只有睿智的河水知道
[00:54.04]白雪是因为贪玩跑出了城堡
[00:57.80]小红帽有件抑制自己
[01:01.02]变成狼的大红袍
[01:04.55]总有一条蜿蜒在童话镇里七彩的河
[01:08.68]沾染魔法的乖张气息
[01:13.55]却又在爱里曲折
[01:18.84]川流不息扬起水花
[01:22.18]又卷入一帘时光入水
[01:25.41]让所有很久很久以前
[01:29.01]都走到幸福结局的时刻
[01:48.70]听说睡美人被埋藏
[01:50.81]小人鱼在眺望金殿堂
[01:54.11]听说阿波罗变成金乌
[01:57.74]草原有奔跑的剑齿虎
[02:01.10]听说匹诺曹总说着谎
[02:04.72]侏儒怪拥有宝石满箱
[02:07.96]听说悬崖有颗长生树
[02:11.57]红鞋子不知疲倦地在跳舞
[02:15.03]只有睿智的河水知道
[02:18.28]睡美人逃避了生活的煎熬
[02:22.01]小人鱼把阳光抹成眼影
[02:25.09]投进泡沫的怀抱
[02:28.31]总有一条蜿蜒在童话镇里七彩的河
[02:35.56]沾染魔法的乖张气息
[02:40.60]却又在爱里曲折
[02:42.92]川流不息扬起水花
[02:45.99]又卷入一帘时光入水
[02:49.23]让所有很久很久以前
[02:53.11]都走到幸福结局的时刻
[02:56.34]总有一条蜿蜒在童话镇里梦幻的河
[03:03.43]分隔了理想分隔现实
[03:07.73]又在前方的山口汇合
[03:10.38]川流不息扬起水花
[03:13.86]又卷入一帘时光入水
[03:16.79]让所有很久很久以前
[03:20.11]都走到幸福结局的时刻
[03:24.19]又陌生
[03:33.22]`;
const lrc = `[00:01.78]童话镇 - 许娜
[00:03.14]词:竹君
[00:05.23]曲:暗杠
[00:06.92]录制:文克津
[00:24.50]听说白雪公主在逃跑
[00:26.74]小红帽在担心大灰狼
[00:29.91]听说疯帽喜欢爱丽丝
[00:33.39]丑小鸭会变成白天鹅
[00:36.89]听说彼得潘总长不大
[00:40.24]杰克他有竖琴和魔法
[00:43.75]听说森林里有糖果屋
[00:47.23]灰姑娘丢了心爱的玻璃鞋
[00:51.13]只有睿智的河水知道
[00:54.04]白雪是因为贪玩跑出了城堡
[00:57.80]小红帽有件抑制自己
[01:01.02]变成狼的大红袍
[01:04.55]总有一条蜿蜒在童话镇里七彩的河
[01:08.68]沾染魔法的乖张气息
[01:13.55]却又在爱里曲折
[01:18.84]川流不息扬起水花
[01:22.18]又卷入一帘时光入水
[01:25.41]让所有很久很久以前
[01:29.01]都走到幸福结局的时刻
[01:48.70]听说睡美人被埋藏
[01:50.81]小人鱼在眺望金殿堂
[01:54.11]听说阿波罗变成金乌
[01:57.74]草原有奔跑的剑齿虎
[02:01.10]听说匹诺曹总说着谎
[02:04.72]侏儒怪拥有宝石满箱
[02:07.96]听说悬崖有颗长生树
[02:11.57]红鞋子不知疲倦地在跳舞
[02:15.03]只有睿智的河水知道
[02:18.28]睡美人逃避了生活的煎熬
[02:22.01]小人鱼把阳光抹成眼影
[02:25.09]投进泡沫的怀抱
[02:28.31]总有一条蜿蜒在童话镇里七彩的河
[02:35.56]沾染魔法的乖张气息
[02:40.60]却又在爱里曲折
[02:42.92]川流不息扬起水花
[02:45.99]又卷入一帘时光入水
[02:49.23]让所有很久很久以前
[02:53.11]都走到幸福结局的时刻
[02:56.34]总有一条蜿蜒在童话镇里梦幻的河
[03:03.43]分隔了理想分隔现实
[03:07.73]又在前方的山口汇合
[03:10.38]川流不息扬起水花
[03:13.86]又卷入一帘时光入水
[03:16.79]让所有很久很久以前
[03:20.11]都走到幸福结局的时刻
[03:24.19]又陌生
[03:33.22]`;