【经典动效】滚动歌词之《罗刹海市》

近期浏览了一堂公开课讲的《滚动歌词》的经典动效案例,这里我将通过文字按我的开发思路和顺序来进行讲述和实现。

  1. 实现静态页面
  2. 验证滚动方案
  3. 解析歌词数据
  4. 插入歌词元素
  5. 实现滚动方案
  6. 偏移公式讲解

实现静态页面

首先在htmlbody部分增加一个container,用来包裹由每一行歌词文本组成的ul无序列表:

<body>

    <div class="container">
        <ul>
            <li>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Dicta, doloribus.</li>
            <li>Nulla, porro nisi quisquam sunt laudantium fugiat odit quas ipsa!</li>
            <li>Debitis, quibusdam in illo perferendis voluptates beatae ratione? Beatae, nisi?</li>
        </ul>
    </div>
</body>

接着对页面做一些样式调整:

  1. 容器样式调整:重置 marginpadding,设置 body 背景色、文本颜色及对齐方式,设置 .container 默认高度并禁止内容溢出(歌词的无序列表要在此容器中进行滚动展示);
* {
    margin: 0;
    padding: 0;
}





body {
    background: #000;
    color: #666;
    text-align: center;
}


.container {
    height: 300px;
    overflow: hidden;
}

  1. 歌词无序列表初始化位置:默认将歌词无序列表的位置移动到 .container 容器垂直居中,也就是 容器高度/2 - 每行歌词高度/2 = 135px 的位置,在进行移动时优先使用 transform 进行非几何属性的变化,避免回流(reflow)频繁发生;
.container ul {

    transform: translateY(135px);

}
  1. 每行歌词的样式调整:设置每行歌词的高度,并给定一个与高度一致的 line-height,让歌词文本居中显示;还会增加一个 active 样式类,用来高亮并放大显示当前播放的歌词;
.container li {

    height: 30px;

    line-height: 30px;

}





.container li.active {
    color: #ccc;
    transform: scale(1.2);
}


增加过渡效果:

歌词无序列表的滚动和当前播放歌词的放大目前都是生硬的,需要为它们增加一些过渡效果,这样会顺滑一些。这里会用到 transition 属性,可以指定要生效的 property nameduration

  1. 歌词无序列表滚动:
.container ul {

    transform: translateY(135px);

    transition: transform 0.5s;
}


  1. 当前播放歌词放大:
.container li {

    height: 30px;

    line-height: 30px;

    transition: transform 0.5s;
}

注:这部分内容仅为静态内容,页面样式及动效通过控制台修改参数来进行简单验证。

验证滚动方案

音频播放需要用到 audio 标签,所以首先要在页面中插入一个 audio 标签;

<body>

    <audio controls src="./assets/2177806392.mp3"></audio>
</body>

audio 在对音频进行播放期间当 currentTime 更新时会触发 timeupdate 事件,也就是说,我们可以通过监听 timeupdate 事件来获取当前播放的位置(时间)currentTime(单位:秒)。

const audio = document.querySelector("audio");
audio.addEventListener("timeupdate", () =>
    console.log(`当前播放到 ${audio.currentTime} 秒`)
);

播放的时间搞定后,就需要与歌词文件进行匹配,歌词文件选择包含时间的 lrc 文件,在 lrc 文件中每一行为一组包含了时间和歌词的数据,时间是由 [分:秒] 组成的,需要将时间进行一定的转换后在与 audocurrentTime 进行匹配;

function paresTime() {
    const times = timeStr && timeStr.split(":");
    if (times && times.length === 2) {
        const minute = times[0];
        const second = times[1];
        return +minute * 60 + +second;
    }
    return 0;
}


解析歌词数据

通过 fetch 函数加载Lrc歌词文件并将歌词数据对象化处理:会通过 splitslicemap 进行处理;

async function getLrcData() {
    const data = await fetch("./assets/罗刹海市.lrc").then((response) =>
        response.text()
    );
    return data.split("\n").map((line) => {
        return {
            time: paresTime(line.split("]")[0].slice(1)),
            words: line.split("]")[1],
        };
    });
}

编写一个自执行函数来运行获取歌词;

(async ()=>{
    const data = await getLrcData();
    console.log(data);
})()

插入歌词元素

创建一个 DocumentFragment,接收遍历歌词数据时创建了 li元素,在结束遍历后统一添加到 ul 无序列表中;

function genLrcFragment(data) {
    const fragment = document.createDocumentFragment();
    data.forEach(line => {
        const li = document.createElement('li');
        li.textContent = line.words;
        fragment.appendChild(li);
    });
    return fragment;
}



const fragment = genLrcFragment(data);
document.querySelector('.container ul').appendChild(fragment);

实现滚动方案

  1. 确定当前播放音乐对应歌词在歌词数据中的下标位置:如当前播放时间为 14 秒,那么当前高亮的应该就是 14.36 秒前的一句歌词;但当前播放时间大于 04:32.3 秒(完整音乐播放到05:32秒)时,始终返回歌词数据的最后一个下标位置;
const index = data.findIndex((line) => currentTime < line.time);
const highlightIndex = index != -1 ? index - 1 : data.length - 1;
  1. 获取歌词需要偏移(移动)的距离:某下标[x]的歌词位置 = 容器高度/2 – 每行歌词高度/2 – 每行歌词高度*[x];
let containerHeight = 0;
let liHeight = 0;
let uldom = null;

if (!containerHeight) {
    containerHeight = document.querySelector('.container').clientHeight;
}
if (!liHeight) {
    liHeight = document.querySelector('.container ul').children[0].clientHeight;
}

// 300/2 - 30*1 - 30/2 = 105px
const offset = containerHeight / 2 - liHeight * highlightIndex - liHeight / 2;
if (!uldom) {
    uldom = document.querySelector('.container ul');
}

uldom.style.transform = `translateY(${offset}px)`;
  1. 处理高亮歌词:每次高亮新的歌词之前要移除已高亮部分;
function highlight(data, currentTime) {
    // 移除高亮
    const active = document.querySelector(".active");
    active && active.classList.remove("active");



    const index = data.findIndex((line) => currentTime < line.time);
    const highlightIndex = index != -1 ? index - 1 : data.length - 1;
    if (!containerHeight) {
        containerHeight = document.querySelector('.container').clientHeight;
    }
    if (!liHeight) {
        liHeight = document.querySelector('.container ul').children[0].clientHeight;
    }
    const offset = containerHeight / 2 - liHeight * highlightIndex - liHeight / 2;
    if (!uldom) {
        uldom = document.querySelector('.container ul');
    }
    // 添加新歌词的高亮
    uldom.children[highlightIndex].classList.add("active");
    uldom.style.transform = `translateY(${offset}px)`;
}

偏移公式讲解

公式:某下标[x]的歌词位置 = 容器高度/2 – 每行歌词高度/2 – 每行歌词高度*[x];

我们默认的容器高度为 300px,一半的容器高度就是 150pxul 直接偏移 容器高度/2 后其实并非容器的正中间,而是多偏移了 15px,也就是每行歌词一半的距离,所以需要减去 每行高度/2 的一个距离,剩下的在图中也可以看的出来,下标为 1 的时候 每行歌词高度*1,下标为 2 的时候 每行歌词高度*2。最后实际的偏移高度就如图和公式所示进行相减获得。

总结

在实现案例之前,首先要做的就是熟悉歌词数据,搞清楚歌词数据中包含有哪些内容,同时要结合 audio 标签提供的事件进行监听并获取到实时的播放时间,能相互匹配确认可行后才能着手开发。


如果看完觉得有收获,欢迎点赞、评论、分享支持一下。你的支持和肯定,是我坚持写作的动力~

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYrUaXXL' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片