实现效果

歌词滚动.gif

实现分析

解析歌词字符串转化为对象

var lyrics = `[00:00.000]漂洋过海来看你-孙露
[00:09.480]词:李宗盛
[00:18.960]曲:李宗盛
[00:28.440]为你我用了半年的积蓄
[00:31.980]漂洋过海的来看你
......`;

function parseLyrics() {
    let Things = lyrics.split('\n');
    let result = [];
    for (var i = 0; i < Things.length; i++) {
        let str = Things[i].split(']');
        let timeStr = str[0].substring(1);
        let timeArr = timeStr.split(':');
        let time = +timeArr[0] * 60 + +timeArr[1];
        let obj = {
            time: time,
            words: str[1],
        };
        result.push(obj);
    }
    return result;
}
  • parseLyrics 函数通过对输入的歌词字符串进行分割和处理,将时间部分转换为以秒为单位的数值,并与对应的歌词文本组合成对象,存储在 result 数组中。这样的对象形式方便后续根据时间进行查找和操作。

根据歌词对象和播放器当前播放时间计算当前播放到的歌词


function subscript() {
    let curTime = doms.audio.currentTime;
    for (var i = 0; i < lyricsData.length; i++) {
        if (curTime < lyricsData[i].time) {
            return i - 1;
        }
    }
    return lyricsData.length - 1;
}
  • subscript 函数接受当前音频的播放时间 curTime ,通过遍历歌词对象数组,比较时间来确定当前播放到的歌词索引。

把解析的歌词内容呈现到页面

function initLyricsHtml() {
    let frag = document.createDocumentFragment();
    for (var i = 0; i < lyricsData.length; i++) {
        let li = document.createElement('li');
        li.textContent = lyricsData[i].words;
        frag.appendChild(li);
    }
    doms.ul.appendChild(frag);
}
  • initLyricsHtml 函数创建 li 元素并填充歌词文本,使用 DocumentFragment 来优化性能。DocumentFragment 是一个轻量级的文档片段,它可以容纳多个 DOM 节点,当将其添加到文档中时,只会发生一次页面重绘或回流,从而提高性能。最后将所有 li 元素添加到 ul 中展示歌词。

根据当前播放到的歌词计算出 ul 向上偏移的位置以及当前高亮歌词

QQ截图20240819143328.png

2种边界情况.png

function setOffset() {
    let index = subscript();
    let offset = liHeight * index + liHeight / 2 - containerHeight / 2;
    if (offset < 0) {
        offset = 0;
    }
    if (offset > maxOffset) {
        offset = maxOffset;
    }
    doms.ul.style.transform = `translateY(-${offset}px)`;

    let liActive = doms.ul.querySelector('.active');
    if (liActive) {
        liActive.classList.remove('active');
    }
    let li = doms.ul.children[index];
    if (li) {
        li.classList.add('active');
    }
}
  • 首先计算了一些与页面布局相关的高度值,如容器高度、每行歌词高度和最大偏移量。
  • setOffset 函数根据当前播放的歌词索引计算 ul 的偏移量,确保当前歌词在容器中显示合适,并设置当前高亮的歌词行。使用 translateY 而不是 top 主要是因为 translateY 是通过 CSS 变换来实现元素的位移,它在性能上通常比直接修改 top 属性更优。translateY 可以利用硬件加速,并且不会引起页面的重布局,从而使页面的渲染更加流畅。

给播放器添加监听器监控 timeupdate 事件,执行 ul 偏移

doms.audio.addEventListener('timeupdate',setOffset);
  • doms.audio.addEventListener('timeupdate', setOffset); 这行代码使得在音频播放时间不断更新时,能够及时调用 setOffset 函数来调整歌词的显示位置和高亮状态。

完整的代码

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>歌词滚动</title>
    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
    <link rel="stylesheet" href="index.css" />
</head>
<body>
    <audio controls src="./孙露-漂洋过海来看你.mp3"></audio>
    <div class="container">
        <ul class="lrc-list"></ul>
    </div>
        <canvas></canvas>
    <script src="data.js"></script>
    <script src="index.js"></script>
</body>
</html>

index.css

*{
    margin: 0;
    padding: 0;
}
body{
    background: #000;
    color: #666;
    text-align: center;
}
audio{
    width: 450px;
    margin: 30px 0;
}
.container{
    height: 420px;
    overflow: hidden;
}

.container ul{
    transition: 0.6s;
}

.container li {
    height: 30px;
    line-height: 30px;
    transition: 0.2s;
}

.container li.active{
    color:#fff;
    transform: scale(1.3);
} 

index.js

//定义需要的dom操作对象
var doms = {
    audio: document.querySelector('audio'),
    ul: document.querySelector('.container ul'),
    container: document.querySelector('.container'),
    cvs: document.querySelector('canvas'),

}

//解析歌词字符串
function parseLyrics() {
    let Things = lyrics.split('\n');
    let result = [];
    for (var i = 0; i < Things.length; i++) {
        let str = Things[i].split(']');
        let timeStr = str[0].substring(1);
        let timeArr = timeStr.split(':');
        let time = +timeArr[0] * 60 + +timeArr[1];
        let obj = {
            time: time, words: str[1],
        };
        result.push(obj);
    }
    return result;
}

let lyricsData = parseLyrics();

// 获取歌词的脚本标识
function subscript() {
    let curTime = doms.audio.currentTime;
    for (var i = 0; i < lyricsData.length; i++) {
        if (curTime < lyricsData[i].time) {
            return i - 1;
        }
    }
    return lyricsData.length - 1;
}

//初始化歌词显示
function initLyricsHtml() {
    let frag = document.createDocumentFragment();
    for (var i = 0; i < lyricsData.length; i++) {
        let li = document.createElement('li');
        li.textContent = lyricsData[i].words;
        frag.appendChild(li);
    }
    doms.ul.appendChild(frag);
}

initLyricsHtml();

let containerHeight = doms.container.clientHeight;
let liHeight = doms.ul.children[0].clientHeight;
let maxOffset = doms.ul.clientHeight - containerHeight;

//计算ul的偏移量
function setOffset() {
    let index = subscript();
    let offset = liHeight * index + liHeight / 2 - containerHeight / 2;
    if (offset < 0) {
        offset = 0;
    }
    if (offset > maxOffset) {
        offset = maxOffset;
    }
    doms.ul.style.transform = `translateY(-${offset}px)`;

    let liActive = doms.ul.querySelector('.active');
    if (liActive) {
        liActive.classList.remove('active');
    }
    let li = doms.ul.children[index];
    if (li) {
        li.classList.add('active');
    }
}

//监听歌曲播放
doms.audio.addEventListener('timeupdate', setOffset);

//画圆

var ctx = doms.cvs.getContext('2d', {alpha: false, desynchronized: false, antialias: true});

function initCvs() {
    let size = 500;
    // 内部绘图区域的大小
    doms.cvs.width = size * devicePixelRatio;
    doms.cvs.height = size * devicePixelRatio;
    // 页面布局中的显示大小
    doms.cvs.style.width = doms.cvs.style.height = size + 'px';
}

initCvs();

function draw(data, maxVal) {

    const centerX = doms.cvs.width / 2;//圆心坐标x
    const centerY = doms.cvs.height / 2;//圆心坐标y
    const r = 50;//圆半径
    const numberList = data.map(item => Math.min(item, maxVal));
    const len = numberList.length;

    const angle = 2 * Math.PI;

    ctx.clearRect(0, 0, doms.cvs.width, doms.cvs.height);
    ctx.beginPath();//开启一个路径

    // 创建渐变
    let gradient = ctx.createLinearGradient(centerX, centerY, centerX + r, centerY);
    gradient.addColorStop(0, 'rgb(65,1,62,0.5)');
    gradient.addColorStop(0.25, 'rgba(3,121,75,0.5)');
    gradient.addColorStop(0.5, 'rgba(3,103,3,0.5)');
    gradient.addColorStop(0.75, 'rgb(87,58,2,0.5)');
    gradient.addColorStop(1, 'rgb(105,13,3,0.5)');

    let j = 0;
    for (let i = 0; i < angle; i += Math.PI / len) {
        let x = centerX + r * Math.cos(i);
        let y = centerY + r * Math.sin(i);
        ctx.fillStyle = gradient;
        ctx.fillRect(x, y, 1, 1);
        let increase = numberList[j];
        j++;
        if (j >= len) {
            j = 0;
        }
        let offsetX = centerX + (r + increase) * Math.cos(i);
        let offsetY = centerY + (r + increase) * Math.sin(i);

        ctx.moveTo(x, y);
        ctx.lineTo(offsetX, offsetY);
        ctx.strokeStyle = gradient;
        ctx.lineWidth = 1;
        ctx.stroke();
    }
}

let isInit = false;
let analyser;
let buffer;
doms.audio.onplay = function () {
    if (isInit) {
        return;
    }
    //创建一个新的音频上下文
    const audioCtx = new AudioContext();
    //创建一个音频分析节点
    analyser = audioCtx.createAnalyser();

    //创建Uint8Array类型的数组
    analyser.fftSize = 512;
    buffer = new Uint8Array(analyser.frequencyBinCount);

    //音频来源节点
    const source = audioCtx.createMediaElementSource(doms.audio);

    //音频来源节点 和 分析器节点连接
    source.connect(analyser);

    //分析完成, 分析器 连接 上下文 把 音频输出到设备
    analyser.connect(audioCtx.destination);
    isInit = true;
}

//分析器
function audioLoop() {
    requestAnimationFrame(audioLoop);
    if (!isInit) {
        return;
    }
    //获取音频的频率数据并以字节数组的形式返回
    //参数是一个Uint8Array类型的数组,这个数组用于存储返回的频率数据。
    analyser.getByteFrequencyData(buffer);
    //处理有些频率没功率,我们可以截取小段。
    const offset = Math.floor(buffer.length * 3 / 10);
    const data = new Array(offset*2);
    for (let i = 0; i < offset; i++) {
        data[i] = data[data.length - i -1] =  buffer[i];
    }
    draw(data, 80);
}

audioLoop();
Last modification:August 23, 2024
如果觉得我的文章对你有用,请随意赞赏