html 歌词同步滚动实现思路深入解析

效果展示

跟随歌曲时间同步滚动歌词,并实现高亮效果

自动播放

改变进度

总体思路

  1. 获取歌词
  2. 解析歌词
  3. 打印歌词
  4. 同步歌词

1. 获取歌词

用 ajax 和网易云的 api 获取的歌词资源.(涉及到跨域问题,这里不赘述,主要讲同步功能的思路)

2. 解析歌词

步骤:
1. 新建数组 lrcArray
2. 提取歌词 lrcGet
3. 用换行符把字符串 lrcGet分 割为数组 lrc
4. 遍历 lrc

其中,遍历 lrc 后的处理步骤

  • 过滤
  • 提取和转化时间
  • 提取歌词
  • 添加进数组 lrcArray

控制台返回的 lrcGet

lrc

js

      var lrcArray = [];//新建数组,用于存放歌词
      var lrcGet = data.lrc.lyric;//提取歌词
      // console.log(lrcGet);
      var lrc = lrcGet.split('\n');
      // console.log(lrc);


      $.each(lrc, function(i, item) {
        //过滤空白文本
        if (item.split(']')[1] == "" || item == "" || item.indexOf('作曲') !== -1 || item.indexOf('作词') !== -1) {
          return true;
      }
        //转化时间
        var timeStr = item.substring(item.indexOf("[") + 1, item.indexOf("]"));
        var min = parseInt(timeStr.split(':')[0]) * 60;
        var sec = parseFloat(timeStr.split(':')[1]);
        var time = parseFloat((min + sec).toFixed(2));
        //添加进数组
        lrcArray.push({
          t: time,
          c: item.substring(item.indexOf(']') + 1)
        });
      });

3. 打印歌词

控制台返回处理后的数组lrcArray如下:

html代码

      <div class="lyrics">
        <ul class="lyricsList"></ul>
      </div>

js

      //显示歌词
      //打印全部在页面
      var html = "";
      $.each(lrcArray, function(i, v) {
        html += '<li>' + v.c + '</li>';
      });
      $('.lyricsList').append(html);

4. 同步歌词

      $('#audio')[0].ontimeupdate = function() {
        $.each(lrcArray, function(i, v) {
          if ($('#audio')[0].currentTime >= lrcArray[i].t) {
            $('.lyricsList').css('margin-top', '');//避免进度变动时数值产生混乱
            $('.lyricsList li').eq(i).addClass('highlight');
            $('.lyricsList li').eq(i).siblings().removeClass('highlight');
            if (i > 2) {
              $('.lyricsList').css('margin-top', (-i + 2) * 30 + 'px');
            }
          }
        });
      };

思路: audio 时间进度每更新一次,就同步一次该数组,把audio 的当前时间和数组每一项的时间作比较.当检测到前者大于或等于后者,就高亮和滚动对应的歌词.

关于高亮的思路

以代码的角度:

以index(这里对应 i)为桥梁,只要符合条件,对应的 li 就会先被高亮后被消除高亮,一直到最后一个符合条件的 lrcArray[i].t里的 i对应的 li被高亮,此时只会高亮而不会消除,等到 i+1 符合条件,i 的 先高亮后消除,i+1只高亮,以此迭代下去.

从样式上看:

由于代码执行飞速,以致肉眼不会看到先高亮后消除的过程,只会看到最后一个被高亮.

关于滚动的思路

从第一行歌词开始滚动,定格在第三行

html代码

      <div class="lyrics">
        <ul class="lyricsList"></ul>
      </div>

css 样式

    .lyrics {
      height: 32%;
      padding-top: 8px;
      overflow: hidden;
    }
    .lyrics .lyricsList {
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }
    .lyrics .lyricsList li {
      height: 30px;
      line-height: 15px;
      font-size: 12px;
    }

js 代码

      var index = 0;
      $('#audio')[0].ontimeupdate = function() {
        if (this.currentTime >= parseFloat(lrcArray[index].t)) {
          $('.lyricsList li').eq(index).addClass('highlight');
          $('.lyricsList li').eq(index).siblings().removeClass('highlight');
          if (index > 2) {
            //添加 css 样式以实现滚动效果
            $('.lyricsList').css('margin-top', -(index - 2) * 30 + 'px');
          }
          index++;
        }
      };

js 滚动思路分析图

从 i>2 起设置margin-top负值.
当滚动到 i=3对应的 li 节点后,margin-top 的值每次增加一个负的 li 的行高,即-30px,以实现推动效果。在高亮效果滚动到下一个 li的同时,ul 容器往上推动一个li行高,两者作用下的效果为高亮位置保持不变。

注:

  • 在条件代码块的第一行用.css('margin-top', '')消除样式,避免每一次margin-top 值发生改变引起数值错乱,避免 bug.

  • 以下在触发进度条事件时,会发生同步错误.
    原因:
    this.currentTime突然变小,而 index 无法随之改变,因此条件无法匹配.
    this.currentTime突然变大, index也无法突变,只能递增,会产生逐行高亮又消失,直到匹配到临界值的情况.

涉及的 jq 方法总结

  • dom 方法
    • $(dom).eq(i) 返回第i 个;
    • $(dom).siblings() dom的兄弟节点,即除了自己,借助该方法可实现兄弟节点之前的排他性样式,如歌词高亮.
    • $(dom).empty() 清空所有子节点
    • $(dom).css('属性', ''); 消除某个属性
  • each遍历中跳出循环的方法:
    • return false;——跳出所有循环;相当于 javascript 中的 break 效果。
    • return true;——跳出当前循环,进入下一个循环;相当于 javascript 中的 continue 效果
  • 过滤空白文本和包含关键词文本
  if (item.split(']')[1] == "" || item == "" || item.indexOf('作曲') !== -1 || item.indexOf('作词') !== -1) {
    return true;
  • 字符串
    • str.split('\n')
    • str.substring(str.indexOf("[") + 1, str.indexOf("]")) 提取[]中间的字符串
    • str.indexOf(str,str)
  • 数字
    • str.parseInt(),str.parseFloat()
    • num.toFixed(num)
  • 数组
    • arr.push(item)

完整代码

      <div class="lyrics">
        <ul class="lyricsList"></ul>
      </div>
    .lyrics {
      height: 32%;
      padding-top: 8px;
      overflow: hidden;
    }
    .lyrics .lyricsList {
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }
    .lyrics .lyricsList li {
      height: 30px;
      line-height: 15px;
      font-size: 12px;
    }
    //歌词高亮
    .highlight {
      color: #3effee;
      text-shadow: 0 0 10px #ded1fc;
    }
function parseLrc(data) {
  // console.log(data);
  var lrcArray = []; //新建数组,用于存放歌词
  var lrcGet = data.lrc.lyric; //提取歌词
  console.log(lrcGet);
  var lrc = lrcGet.split('\n');
  console.log(lrc);


  $.each(lrc, function(i, item) {
    //过滤空白文本
    if (item.split(']')[1] == "" || item == "" || item.indexOf('作曲') !== -1 || item.indexOf('作词') !== -1) {
      return true;
    }
    //转化时间
    var timeStr = item.substring(item.indexOf("[") + 1, item.indexOf("]"));
    var min = parseInt(timeStr.split(':')[0]) * 60;
    var sec = parseFloat(timeStr.split(':')[1]);
    var time = parseFloat((min + sec).toFixed(2));
    //添加进数组
    lrcArray.push({
      t: time,
      c: item.substring(item.indexOf(']') + 1)
    });
  });

  console.log(lrcArray);

  //显示歌词
  var html = "";
  $.each(lrcArray, function(i, v) {
    html += '<li>' + v.c + '</li>';
  });
  $('.lyricsList').append(html);

  //同步高亮歌词
  $('#audio')[0].ontimeupdate = function() {
    $.each(lrcArray, function(i, v) {
      if ($('#audio')[0].currentTime >= lrcArray[i].t) {
        $('.lyricsList').css('margin-top', '');
        $('.lyricsList li').eq(i).addClass('highlight');
        $('.lyricsList li').eq(i).siblings().removeClass('highlight');
        if (i > 2) {
          $('.lyricsList').css('margin-top', (-i + 2) * 30 + 'px');
        }
      }
    });
  };
}

本文作者: 乔一亖
本文链接: https://www.cnblogs.com/joyce33/p/13376752.html
版权声明: 本文版权归作者和博客园共有,转载请注明出处!如有问题或建议,请多多赐教,非常感谢。


posted @   EzenLee  阅读(3530)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示