马黑黑 发表于 2025-3-19 14:27

原生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>

马黑黑 发表于 2025-3-19 14:27

代码:

<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>

马黑黑 发表于 2025-3-19 14:36

歌词滚动机制采用元素级别的 scroll 机制,不像 scrollIntoview API 那样强制回到歌词所在区域,播放器可以放在帖子里。

播放器UI采用 flex 弹性布局,应该完美兼容现代浏览器。

LRC歌词的解析采用我们之前的巧妙做法,利用精心设计的正则表达式,以较少的开销读取到原生LRC歌词信息。

要求所提供的 LRC 歌词不能有误,因为读取歌词处理机制中没有做排错处理。

梦江南 发表于 2025-3-19 16:12

谢谢老师辛苦,学习了。{:4_190:}

马黑黑 发表于 2025-3-19 19:21

梦江南 发表于 2025-3-19 16:12
谢谢老师辛苦,学习了。

{:4_191:}

花飞飞 发表于 2025-3-19 19:22

这歌真好听啊,刚不心小听到了一下。。{:4_173:}
小播界面漂亮,配色高级。。
这是重新剥离出来的呀。

马黑黑 发表于 2025-3-19 19:30

花飞飞 发表于 2025-3-19 19:22
这歌真好听啊,刚不心小听到了一下。。
小播界面漂亮,配色高级。。
这是重新剥离出来的呀。

这个是全新徒手写的,修改的时候少量参考了昨天的东东

花飞飞 发表于 2025-3-19 19:35

马黑黑 发表于 2025-3-19 14:36
歌词滚动机制采用元素级别的 scroll 机制,不像 scrollIntoview API 那样强制回到歌词所在区域,播放器可以 ...

无误的LRC歌词获得还需要你之前提供的方法制作才比较快,又试一次豆包,它把我设的空格自动取消了,严重不准。。这个靠不住。{:4_173:}

花飞飞 发表于 2025-3-19 19:38

马黑黑 发表于 2025-3-19 14:36
歌词滚动机制采用元素级别的 scroll 机制,不像 scrollIntoview API 那样强制回到歌词所在区域,播放器可以 ...

小播歌词显示的方法不太一样,三行同显,高亮当前。。
按纽沿用了裁剪形式,标准,好看。
还是不能强制回歌词所在区域,那页面比兔子跑得还快,太不好逮了。。{:4_170:}

花飞飞 发表于 2025-3-19 19:39

这个以歌词为主了,进度条细细的一小条,简洁美观。

花飞飞 发表于 2025-3-19 19:41

马黑黑 发表于 2025-3-19 19:30
这个是全新徒手写的,修改的时候少量参考了昨天的东东

难怪看着这么顺眼。。原来全新徒手写的。。手掌辛苦啦。{:4_173:}

红影 发表于 2025-3-19 19:48

这个好,可以放到帖子里了。歌词的显示也漂亮,可以出来三行歌词,并高亮显示正在唱的。
很漂亮{:4_199:}

红影 发表于 2025-3-19 20:05

记得黑黑做过三行歌词的,刚才去找了一下。倒是可以和这个比较一下了{:4_187:}

马黑黑 发表于 2025-3-19 20:05

红影 发表于 2025-3-19 19:48
这个好,可以放到帖子里了。歌词的显示也漂亮,可以出来三行歌词,并高亮显示正在唱的。
很漂亮

出几行歌词取决于LRC歌词区域的高度,一般的计算原理是该区域所设置的行高*行数,比如行高设置为 30px,则LRC歌词区域高度设为 150px 即可

马黑黑 发表于 2025-3-19 20:06

红影 发表于 2025-3-19 20:05
记得黑黑做过三行歌词的,刚才去找了一下。倒是可以和这个比较一下了

那是的实现机制好像不是酱紫的,不记得了

马黑黑 发表于 2025-3-19 20:11

花飞飞 发表于 2025-3-19 19:41
难怪看着这么顺眼。。原来全新徒手写的。。手掌辛苦啦。

脚掌不辛苦

马黑黑 发表于 2025-3-19 20:13

花飞飞 发表于 2025-3-19 19:35
无误的LRC歌词获得还需要你之前提供的方法制作才比较快,又试一次豆包,它把我设的空格自动取消了,严重 ...

有时候豆包需要鼓励。谈话前,你得给它个预设:听说你是前端编程高手,写代码一顶一的,我慕名前来,想让你给我做一个单页面的音频播放器,具体要求是1234567

马黑黑 发表于 2025-3-19 20:14

花飞飞 发表于 2025-3-19 19:38
小播歌词显示的方法不太一样,三行同显,高亮当前。。
按纽沿用了裁剪形式,标准,好看。
还是不能强制 ...

现在这个机制歌词和播放器同放在一个父元素里,它们不能分离,觉得也没有必要分离。

红影 发表于 2025-3-19 20:55

马黑黑 发表于 2025-3-19 20:05
出几行歌词取决于LRC歌词区域的高度,一般的计算原理是该区域所设置的行高*行数,比如行高设置为 30px, ...

原来是和空间有关的呢{:4_187:}

花飞飞 发表于 2025-3-19 20:56

马黑黑 发表于 2025-3-19 20:11
脚掌不辛苦

三万步也辛苦。。踩踩。。。{:4_170:}
页: [1] 2 3 4 5
查看完整版本: 原生LRC歌词播放器(初稿)