原生LRC歌词播放器(初稿)
<style>#mplayer { --prg: 0%; margin: 20px auto; padding: 10px; display: flex; gap: 10px; flex-direction: column; align-items: center; width: 400px; background: rgba(0,0,0,.7); color: white; }
#lrcDiv { padding: 8px 0; width: 100%; height: 80px; overflow: hidden; text-align: center; box-sizing: border-box; }
#lrcDiv > p { margin: 0; padding: 0; font: normal 18px/26px simsun, arial, serif; transition: .5s; }
#lrcDiv p.hlight { color: yellow; font-size: 24px; font-weight: bold; }
#btns-area { width: 100%; display: flex; justify-content: space-between; }
#btn-play { width: 20px; height: 20px; cursor: pointer; position: relative; }
#btn-play::after { position: absolute; content: ''; width: 100%; height: 100%; background: red; clip-path: var(--clip); }
#progDiv { width: 100%; height: 10px; background: linear-gradient(to right, red var(--prg), gray var(--prg), gray 0) no-repeat 0 50% / 100% 2px; cursor: pointer; }
.play { --clip: polygon(10% 0, 100% 50%, 10% 100%); }
.pause { --clip: polygon(40% 0, 40% 100%, 20% 100%, 20% 0, 80% 0, 80% 100%, 60% 100%, 60% 0); }
</style>
<div id="mplayer">
<div id="lrcDiv"></div>
<div id="btns-area">
<div id="time1">00:00</div>
<div id="btn-play" class="pause"></div>
<div id="time2">00:00</div>
</div>
<div id="progDiv"></div>
</div>
<audio src="https://music.163.com/song/media/outer/url?id=1985126304" autoplay loop></audio>
<script>
const mplayer = document.querySelector('#mplayer');
const aud = document.querySelector('audio');
const lrcDiv = document.querySelector('#lrcDiv');
const progDiv = document.querySelector('#progDiv');
const btnPlay = document.querySelector('#btn-play');
const vids = document.querySelectorAll('video');
let lrcAr = [];
//处理LRC歌词
const getLrcAr = (text) => {
const ar = text.trim().split('\n');
ar.sort();
var reg = /\[(\d+)[.:](\d+)[.:](\d+)\](.*)/;
ar.forEach(item => {
let result = item.match(reg);
let tmsg = parseInt(result) * 60 + parseInt(result) + parseInt(result) / 1000;
lrcAr.push(.trim()]);
let p = document.createElement('p');
p.innerText = result.trim();
lrcDiv.appendChild(p);
});
};
//渲染播放器
const updatePlayerDatas = () => {
const hh = lrcDiv.offsetHeight / 2 + 20;
const prg = (aud.currentTime / aud.duration) * 100;
time1.innerText = formatTime(aud.currentTime);
time2.innerText = formatTime(aud.duration);
mplayer.style.setProperty('--prg', prg + '%');
for (let i = 0; i < lrcAr.length; i ++) {
const lrc = {time: lrcAr, text: lrcAr};
const next = i < lrcAr.length - 1 ? lrcAr : null;
const p = lrcDiv.children;
if (aud.currentTime >= lrc.time && (!next || aud.currentTime < next)) {
p.classList.add('hlight');
lrcDiv.scroll({left: 0, top: p.offsetTop - hh, behavior: 'smooth'});
} else {
p.classList.remove('hlight');
}
}
};
//时间格式化输出
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
//联动管控机制
const mState = () => {
if (aud.paused) {
btnPlay.className = 'play';
btnPlay.title = '点击播放';
playvids(false);
} else {
btnPlay.className = 'pause';
btnPlay.title = '点击暂停';
playvids(true);
}
};
//播放视频
const playvids = (flag) => {
if(!vids) return;
vids.forEach(vid => flag ? vid.play() : vid.pause());
};
//进度条操作
progDiv.addEventListener('click', (e) => aud.currentTime = e.offsetX / progDiv.offsetWidth * aud.duration);
progDiv.addEventListener('mousemove', (e) => progDiv.title = formatTime(e.offsetX / progDiv.offsetWidth * aud.duration));
const lrc = `好不舍得连呼吸都变得苦涩\n热情过头换来的却只有冷漠\n你只会装可怜人设\n而我才是受害者\n我是撞了南墙都不回头的人\n明明已遍体鳞伤还不肯承认\n我讨厌你谎话里虚构的身份\n以为的天真其实是愚蠢\n我是撞了南墙都不回头的人\n还以为等待我的总有一扇门\n门里的人会用温柔的口吻\n能让我安稳\n好不舍得连呼吸都变得苦涩\n热情过头换来的却只有冷漠\n你只会装可怜人设\n而我才是受害者\n我是撞了南墙都不回头的人\n明明已遍体鳞伤还不肯承认\n我讨厌你谎话里虚构的身份\n以为的天真其实是愚蠢\n我是撞了南墙都不回头的人\n还以为等待我的总有一扇门\n门里的人会用温柔的口吻\n能让我安稳\n如今和你一起的回忆都化作泡沫\n我却还是不忍心戳破\n你的轮廓慢慢的慢慢的消逝褪色\n却还是没有放过我\n我是撞了南墙都不回头的人\n还以为等待我的总有一扇门\n门里的人会用温柔的口吻\n能让我安稳`;
getLrcAr(lrc);
aud.addEventListener('timeupdate', updatePlayerDatas);
aud.addEventListener('playing', mState);
aud.addEventListener('pause', mState);
btnPlay.addEventListener('click', () => aud.paused ? aud.play() : aud.pause());
</script> 代码:
<style>
#mplayer { --prg: 0%; margin: 20px auto; padding: 10px; display: flex; gap: 10px; flex-direction: column; align-items: center; width: 400px; background: rgba(0,0,0,.7); color: white; }
#lrcDiv { padding: 8px 0; width: 100%; height: 80px; overflow: hidden; text-align: center; box-sizing: border-box; }
#lrcDiv > p { margin: 0; padding: 0; font: normal 18px/26px simsun, arial, serif; transition: .5s; }
#lrcDiv p.hlight { color: yellow; font-size: 24px; font-weight: bold; }
#btns-area { width: 100%; display: flex; justify-content: space-between; }
#btn-play { width: 20px; height: 20px; cursor: pointer; position: relative; }
#btn-play::after { position: absolute; content: ''; width: 100%; height: 100%; background: red; clip-path: var(--clip); }
#progDiv { width: 100%; height: 10px; background: linear-gradient(to right, red var(--prg), gray var(--prg), gray 0) no-repeat 0 50% / 100% 2px; cursor: pointer; }
.play { --clip: polygon(10% 0, 100% 50%, 10% 100%); }
.pause { --clip: polygon(40% 0, 40% 100%, 20% 100%, 20% 0, 80% 0, 80% 100%, 60% 100%, 60% 0); }
</style>
<div id="mplayer">
<div id="lrcDiv"></div>
<div id="btns-area">
<div id="time1">00:00</div>
<div id="btn-play" class="pause"></div>
<div id="time2">00:00</div>
</div>
<div id="progDiv"></div>
</div>
<audio src="https://music.163.com/song/media/outer/url?id=1985126304" autoplay loop></audio>
<script>
const mplayer = document.querySelector('#mplayer');
const aud = document.querySelector('audio');
const lrcDiv = document.querySelector('#lrcDiv');
const progDiv = document.querySelector('#progDiv');
const btnPlay = document.querySelector('#btn-play');
const vids = document.querySelectorAll('video');
let lrcAr = [];
//处理LRC歌词
const getLrcAr = (text) => {
const ar = text.trim().split('\n');
ar.sort();
var reg = /\[(\d+)[.:](\d+)[.:](\d+)\](.*)/;
ar.forEach(item => {
let result = item.match(reg);
let tmsg = parseInt(result) * 60 + parseInt(result) + parseInt(result) / 1000;
lrcAr.push(.trim()]);
let p = document.createElement('p');
p.innerText = result.trim();
lrcDiv.appendChild(p);
});
};
//渲染播放器
const updatePlayerDatas = () => {
const hh = lrcDiv.offsetHeight / 2 + 20;
const prg = (aud.currentTime / aud.duration) * 100;
time1.innerText = formatTime(aud.currentTime);
time2.innerText = formatTime(aud.duration);
mplayer.style.setProperty('--prg', prg + '%');
for (let i = 0; i < lrcAr.length; i ++) {
const lrc = {time: lrcAr, text: lrcAr};
const next = i < lrcAr.length - 1 ? lrcAr : null;
const p = lrcDiv.children;
if (aud.currentTime >= lrc.time && (!next || aud.currentTime < next)) {
p.classList.add('hlight');
lrcDiv.scroll({left: 0, top: p.offsetTop - hh, behavior: 'smooth'});
} else {
p.classList.remove('hlight');
}
}
};
//时间格式化输出
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
//联动管控机制
const mState = () => {
if (aud.paused) {
btnPlay.className = 'play';
btnPlay.title = '点击播放';
playvids(false);
} else {
btnPlay.className = 'pause';
btnPlay.title = '点击暂停';
playvids(true);
}
};
//播放视频
const playvids = (flag) => {
if(!vids) return;
vids.forEach(vid => flag ? vid.play() : vid.pause());
};
//进度条操作
progDiv.addEventListener('click', (e) => aud.currentTime = e.offsetX / progDiv.offsetWidth * aud.duration);
progDiv.addEventListener('mousemove', (e) => progDiv.title = formatTime(e.offsetX / progDiv.offsetWidth * aud.duration));
const lrc = `好不舍得连呼吸都变得苦涩\n热情过头换来的却只有冷漠\n你只会装可怜人设\n而我才是受害者\n我是撞了南墙都不回头的人\n明明已遍体鳞伤还不肯承认\n我讨厌你谎话里虚构的身份\n以为的天真其实是愚蠢\n我是撞了南墙都不回头的人\n还以为等待我的总有一扇门\n门里的人会用温柔的口吻\n能让我安稳\n好不舍得连呼吸都变得苦涩\n热情过头换来的却只有冷漠\n你只会装可怜人设\n而我才是受害者\n我是撞了南墙都不回头的人\n明明已遍体鳞伤还不肯承认\n我讨厌你谎话里虚构的身份\n以为的天真其实是愚蠢\n我是撞了南墙都不回头的人\n还以为等待我的总有一扇门\n门里的人会用温柔的口吻\n能让我安稳\n如今和你一起的回忆都化作泡沫\n我却还是不忍心戳破\n你的轮廓慢慢的慢慢的消逝褪色\n却还是没有放过我\n我是撞了南墙都不回头的人\n还以为等待我的总有一扇门\n门里的人会用温柔的口吻\n能让我安稳`;
getLrcAr(lrc);
aud.addEventListener('timeupdate', updatePlayerDatas);
aud.addEventListener('playing', mState);
aud.addEventListener('pause', mState);
btnPlay.addEventListener('click', () => aud.paused ? aud.play() : aud.pause());
</script>
歌词滚动机制采用元素级别的 scroll 机制,不像 scrollIntoview API 那样强制回到歌词所在区域,播放器可以放在帖子里。
播放器UI采用 flex 弹性布局,应该完美兼容现代浏览器。
LRC歌词的解析采用我们之前的巧妙做法,利用精心设计的正则表达式,以较少的开销读取到原生LRC歌词信息。
要求所提供的 LRC 歌词不能有误,因为读取歌词处理机制中没有做排错处理。 谢谢老师辛苦,学习了。{:4_190:} 梦江南 发表于 2025-3-19 16:12
谢谢老师辛苦,学习了。
{:4_191:} 这歌真好听啊,刚不心小听到了一下。。{:4_173:}
小播界面漂亮,配色高级。。
这是重新剥离出来的呀。 花飞飞 发表于 2025-3-19 19:22
这歌真好听啊,刚不心小听到了一下。。
小播界面漂亮,配色高级。。
这是重新剥离出来的呀。
这个是全新徒手写的,修改的时候少量参考了昨天的东东 马黑黑 发表于 2025-3-19 14:36
歌词滚动机制采用元素级别的 scroll 机制,不像 scrollIntoview API 那样强制回到歌词所在区域,播放器可以 ...
无误的LRC歌词获得还需要你之前提供的方法制作才比较快,又试一次豆包,它把我设的空格自动取消了,严重不准。。这个靠不住。{:4_173:}
马黑黑 发表于 2025-3-19 14:36
歌词滚动机制采用元素级别的 scroll 机制,不像 scrollIntoview API 那样强制回到歌词所在区域,播放器可以 ...
小播歌词显示的方法不太一样,三行同显,高亮当前。。
按纽沿用了裁剪形式,标准,好看。
还是不能强制回歌词所在区域,那页面比兔子跑得还快,太不好逮了。。{:4_170:} 这个以歌词为主了,进度条细细的一小条,简洁美观。 马黑黑 发表于 2025-3-19 19:30
这个是全新徒手写的,修改的时候少量参考了昨天的东东
难怪看着这么顺眼。。原来全新徒手写的。。手掌辛苦啦。{:4_173:} 这个好,可以放到帖子里了。歌词的显示也漂亮,可以出来三行歌词,并高亮显示正在唱的。
很漂亮{:4_199:} 记得黑黑做过三行歌词的,刚才去找了一下。倒是可以和这个比较一下了{:4_187:} 红影 发表于 2025-3-19 19:48
这个好,可以放到帖子里了。歌词的显示也漂亮,可以出来三行歌词,并高亮显示正在唱的。
很漂亮
出几行歌词取决于LRC歌词区域的高度,一般的计算原理是该区域所设置的行高*行数,比如行高设置为 30px,则LRC歌词区域高度设为 150px 即可 红影 发表于 2025-3-19 20:05
记得黑黑做过三行歌词的,刚才去找了一下。倒是可以和这个比较一下了
那是的实现机制好像不是酱紫的,不记得了 花飞飞 发表于 2025-3-19 19:41
难怪看着这么顺眼。。原来全新徒手写的。。手掌辛苦啦。
脚掌不辛苦 花飞飞 发表于 2025-3-19 19:35
无误的LRC歌词获得还需要你之前提供的方法制作才比较快,又试一次豆包,它把我设的空格自动取消了,严重 ...
有时候豆包需要鼓励。谈话前,你得给它个预设:听说你是前端编程高手,写代码一顶一的,我慕名前来,想让你给我做一个单页面的音频播放器,具体要求是1234567 花飞飞 发表于 2025-3-19 19:38
小播歌词显示的方法不太一样,三行同显,高亮当前。。
按纽沿用了裁剪形式,标准,好看。
还是不能强制 ...
现在这个机制歌词和播放器同放在一个父元素里,它们不能分离,觉得也没有必要分离。 马黑黑 发表于 2025-3-19 20:05
出几行歌词取决于LRC歌词区域的高度,一般的计算原理是该区域所设置的行高*行数,比如行高设置为 30px, ...
原来是和空间有关的呢{:4_187:} 马黑黑 发表于 2025-3-19 20:11
脚掌不辛苦
三万步也辛苦。。踩踩。。。{:4_170:}