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

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

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

实现静态页面

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

html 复制代码
<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 默认高度并禁止内容溢出(歌词的无序列表要在此容器中进行滚动展示);
css 复制代码
* {
    margin: 0;
    padding: 0;
}

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

.container {
    height: 300px;
    overflow: hidden;
}
  1. 歌词无序列表初始化位置:默认将歌词无序列表的位置移动到 .container 容器垂直居中,也就是 容器高度/2 - 每行歌词高度/2 = 135px 的位置,在进行移动时优先使用 transform 进行非几何属性的变化,避免回流(reflow)频繁发生;
css 复制代码
.container ul {
    transform: translateY(135px);
}
  1. 每行歌词的样式调整:设置每行歌词的高度,并给定一个与高度一致的 line-height,让歌词文本居中显示;还会增加一个 active 样式类,用来高亮并放大显示当前播放的歌词;
css 复制代码
.container li {
    height: 30px;
    line-height: 30px;
}

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

增加过渡效果:

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

  1. 歌词无序列表滚动:
css 复制代码
.container ul {
    transform: translateY(135px);
    transition: transform 0.5s;
}
  1. 当前播放歌词放大:
css 复制代码
.container li {
    height: 30px;
    line-height: 30px;
    transition: transform 0.5s;
}

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

验证滚动方案

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

html 复制代码
<body>
    <audio controls src="./assets/2177806392.mp3"></audio>
</body>

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

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

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

JavaScript 复制代码
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 进行处理;

JavaScript 复制代码
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],
        };
    });
}

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

JavaScript 复制代码
(async ()=>{
    const data = await getLrcData();
    console.log(data);
})()

插入歌词元素

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

JavaScript 复制代码
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秒)时,始终返回歌词数据的最后一个下标位置;
JavaScript 复制代码
const index = data.findIndex((line) => currentTime < line.time);
const highlightIndex = index != -1 ? index - 1 : data.length - 1;
  1. 获取歌词需要偏移(移动)的距离:某下标x的歌词位置 = 容器高度/2 - 每行歌词高度/2 - 每行歌词高度*x
JavaScript 复制代码
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. 处理高亮歌词:每次高亮新的歌词之前要移除已高亮部分;
JavaScript 复制代码
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 标签提供的事件进行监听并获取到实时的播放时间,能相互匹配确认可行后才能着手开发。


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

相关推荐
mldong10 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排10 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby606111 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角12 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!12 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰12 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi14 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen14 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马15 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒16 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端