JS实现歌词滚动效果

核心思路:

  1. 处理歌词数据:{time:开始时间(s), word:歌词}

  2. 计算出当前播放器对应应该显示的歌词的索引

  3. 创建每句歌词的<li>元素,并将歌词对象的word属性插入列表的textContent
    (优化:可使用DocumentFragment一次性插入DOM)

  4. 计算<ul>的偏移量:let offset = liHeight*currentIndex + liHeight/2 - containerHeight/2
    对边缘情况处理:

    • offset < 0 ? offset = 0 : offset
    • const maxOffset = dom.ul.scrollHeight - containerHeight offset > maxOffset ? offset = maxOffset : offset
  5. 为当前对应的<li>元素添加CSS类

  6. 监听播放器的timeupdate事件


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>歌词滚动案例</title>
    <link rel="shortcut icon" href="./assets/music.svg" type="image/x-icon">
    <link rel="stylesheet" href="./style/index4.css">
</head>
<body>
    <audio src="./assets/告解.mp3" controls></audio>
    <div class="container">
        <ul class="lyricList">
        </ul>
    </div>
    <script src="./js/data.js"></script>
    <script src="./js/index4.js"></script>
</body>
</html>
*{
    margin: 0;
    padding: 0;
}

li{
    list-style: none;
}

body{
    background-color: #000;
    color: #666;
    font-size: 26px;
}

audio{
    width: 600px;
    margin: 10px auto;
    display: block;
}

.container{
    width: 600px;
    height: 800px;
    margin: 50px auto;
    text-align: center;  /* 对其中的行盒子元素实现居中 */
    overflow: hidden;
}

/* 控制容器的相对位置实现歌词的滚动
1. 通过margin-top实现歌词的滚动会导致浏览器重排,影响性能
2. 可以通过transform:translateY()实现歌词的滚动,避免重排 */

.lyricList{
    transition:0.2s;
}

.lyricList li{
    height: 50px;
    line-height: 50px;
    transition: 0.2s;
}

.active{
    color: #fff;
    /* font-size: 2em; // 导致重排 */
    transform: scale(1.2);
}
/**
 * 解析歌词字符串
 * {time: 开始时间, word: 歌词}
 */
function parseLrc(){
    var lines = lrc.split('\n');
    var lrcList = [];
    for(var i = 0; i < lines.length; i++){
        var line = lines[i]
        var obj={
            time: parseTime(line.split('[')[1].split(']')[0]),
            word: line.split(']')[1]
        }
        lrcList.push(obj)
    }
    return lrcList
}

/**
 * 解析时间字符串
 * @param {string} str 时间字符串,格式为mm:ss
 * @returns 时间秒数
 */
function parseTime(str){
    var time = str.split(':');
    var min = Number(time[0]);
    var sec = Number(time[1]);
    return min * 60 + sec;
}

var lrcData = parseLrc()


const dom = {
    audio: document.querySelector('audio'),
    ul: document.querySelector('ul'),
    constainer: document.querySelector('.container')
}

/**
 * 计算出当前播放器对应应该显示的歌词的索引
 */
function findIndex(){
    const currentTime = dom.audio.currentTime;
    for(var i = 0; i < lrcData.length; i++){
        var item = lrcData[i];
        if(item.time > currentTime){
            return i - 1;
        }
    }
    return lrcData.length - 1;
}

function createLrcElement(){
    const frag = document.createDocumentFragment()
    for(let i=0;i<lrcData.length;i++){
        const li = document.createElement('li')
        li.textContent = lrcData[i].word
        frag.appendChild(li)         // 每次创建都会改动DOM树,但是需要等出现性能问题时才针对性优化,不要率先优化
    }
    dom.ul.appendChild(frag)
}
createLrcElement()

/**
 * 设置ul元素偏移量
 */
var containerHeight = dom.constainer.clientHeight
var liHeight = dom.ul.children[0].clientHeight

function setOffset(){
    const currentIndex = findIndex()
    let offset = liHeight*currentIndex + liHeight/2 - containerHeight/2
    offset < 0 ? offset = 0 : offset
    const maxOffset = dom.ul.scrollHeight - containerHeight
    offset > maxOffset ? offset = maxOffset : offset
    dom.ul.style.transform = `translateY(${-offset}px)`
    let li = document.querySelector('.active')
    li?.classList.remove('active')
    li = dom.ul.children[currentIndex]
    li?.classList.add('active')
}

dom.audio.addEventListener('timeupdate', setOffset)
var lrc = `[00:0.0]告解
[00:3.0]演唱:世界之外
[00:4.0]
[00:25.68]那天西城又雨落
[00:28.56]像旧日的惶惑
[00:33.45]心动心乱恍如昨
[00:36.63]我在雨夜失措
[00:40.32]控制失落痴迷执着
[00:44.28]爱是自由的沦落
[00:47.40]我就看着我
[00:50.22]清醒地堕落
[00:55.29]我告诉我没结果
[00:58.08]心动是错
[01:00.36]答案不是我
[01:02.91]强求换不得你为我
[01:05.94]留下与我
[01:08.04]真心亦是错
[01:10.86]我告诉我不应得
[01:13.68]短暂交汇
[01:15.81]依然平行错过
[01:18.63]你不属于我
[01:20.37]只留下我
[01:23.55]清醒地沦落
[01:26.55]求不得
[01:28.05]号码从此不被拨
[01:46.83]像从未出现过
[01:51.54]你离开后天晴过
[01:54.72]我的回忆雨落
[01:58.41]污名荣耀寂寞喧嚣
[02:02.31]爱是明知的执着
[02:05.46]我就看着我
[02:08.67]一再地踏错
[02:13.35]我告诉我不要错
[02:16.17]既知无果
[02:18.39]不要再爱了
[02:21.06]可这个世界多寂寞
[02:24.03]若没有你
[02:26.13]还能做什么
[02:29.16]我告诉我要放过
[02:31.62]成全你我
[02:33.51]给你你想要的
[02:36.72]我愿让你飞
[02:38.52]就留下我
[02:41.43]困守地执着
[02:44.76]不易得
[03:02.13]我告诉我不要错
[03:04.98]既知无果
[03:07.17]不要再爱了
[03:09.84]可这个世界多寂寞
[03:12.81]若不爱你
[03:14.85]还能做什么
[03:17.70]我告诉我要放过
[03:18.27]成全你我
[03:25.26]给你你想要的
[03:25.65]我愿让你飞
[03:27.36]就留下我
[03:30.30]困守地执着
[03:33.54]在等着
[03:55.0]`

QQ20251022-162835

posted @ 2025-10-22 16:45  原语  阅读(6)  评论(0)    收藏  举报