JS原生---歌詞滾動效果案例

来源:https://www.cnblogs.com/changM/archive/2023/03/07/17187331.html
-Advertisement-
Play Games

【開門見山】 實現目標: 需要讓歌詞列表隨著播放的時間更新而滾動,即實時的跟隨歌曲的進度而滾動 效果: ​編輯 需要事先準備的東西: 1.音頻(mp3格式): ​編輯 2.歌詞(詳細): ​編輯 先展示html和css的實現(不重要,自己想怎樣調都行,重點在js的邏輯實現) 1.html: 小tip ...


【開門見山】

實現目標:

需要讓歌詞列表隨著播放的時間更新而滾動,即實時的跟隨歌曲的進度而滾動

效果:

編輯

 需要事先準備的東西:

1.音頻(mp3格式):

編輯

 2.歌詞(詳細):

編輯

 先展示html和css的實現(不重要,自己想怎樣調都行,重點在js的邏輯實現)

1.html:

小tips:

這其中的歌詞列表ul里的li,可以用亂序假文(lorem)先去進行佈局或樣式的調整,後續再傳入歌詞。

如:li*30>lorem3  // 生成三十個li,且每個li中隨機生成三個詞語。

<!DOCTYPE html>
<html lang="en">
<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>Document</title>
    <link rel="shortcut icon" href="./assets/favicon.ico" type="image/x-icon">
    <link rel="stylesheet" href="./css/index.css">
</head>
<body>
    <audio src="./assets/music.mp3" controls></audio>
    <div class="container">
        <ul class="wordList">
        </ul>
    </div>
</body>
<script src="./js/data.js"></script>
<script src="./js/index.js"></script>
</html>

 其中data.js為歌詞的文件,index.js為主要邏輯文件,在下邊js的文件中會展示

 2.css:

* {
    margin: 0;
    padding: 0;
}

ul {
    list-style: none;
}

body {
    background-color: black;
}

audio {
    display: block;
    width: 500px;
    margin: 30px auto;
}

.container {
    height: 420px;
    /* border: 1px solid white; */
    overflow: hidden;
}

.container ul {
    transition: all 0.6s;
    text-align: center;
}

.container li {
    color: #666;
    height: 30px;
    line-height: 30px;
}
.container li.active{ 
    transform: scale(1.3); // 歌詞放大效果
    color: #fff;
}

 

 css實現的幾個細節:

1. 實際上整個歌詞列表ul是很長的,而歌詞的滾動需要依靠ul的上下偏移來實現,會在container 中溢出,所以給container 設置overflow:hidden。

2. 實現寫好要給當前播放的歌詞的樣式,封裝到active類下,為後續js的實現鋪好路。

3.當前播放的歌詞的樣式無非兩個,放大和變色,這裡為什麼放大不用font-size,其實有些講究:

 font-size的改變就導致了元素幾何信息的變化,幾何信息的變化就意味著會導致reflow,會使頁面重新佈局,影響效率。

 

而transform 變形,並不是在渲染主線程中執行,不占用主線程,而是在合成線程中執行,最終的實現也是交給cpu,所以不會導致頁面重新佈局,不影響效率。

重點!JS的實現

1.data.js 歌詞文件:

var lrc = `[00:01.06]難念的經
[00:03.95]演唱:周華健
[00:06.78]
[00:30.96]笑你我枉花光心計
[00:34.15]愛競逐鏡花那美麗
[00:36.75]怕幸運會轉眼遠逝
[00:39.32]為貪嗔喜惡怒著迷
[00:41.99]責你我太貪功戀勢
[00:44.48]怪大地眾生太美麗
[00:47.00]悔舊日太執信約誓
[00:49.66]為悲歡哀怨妒著迷
[00:52.56]啊 捨不得璀燦俗世
[00:57.66]啊 躲不開痴戀的欣慰
[01:02.86]啊 找不到色相代替
[01:08.09]啊 參一生參不透這條難題
[01:13.15]吞風吻雨葬落日未曾彷徨
[01:15.73]欺山趕海踐雪徑也未絕望
[01:18.23]拈花把酒偏折煞世人情狂
[01:20.90]憑這兩眼與百臂或千手不能防
[01:23.76]天闊闊雪漫漫共誰同航
[01:26.09]這沙滾滾水皺皺笑著浪蕩
[01:28.68]貪歡一刻偏教那女兒情長埋葬
[01:32.38]
[01:34.09]吞風吻雨葬落日未曾彷徨
[01:36.50]欺山趕海踐雪徑也未絕望
[01:39.07]拈花把酒偏折煞世人情狂
[01:41.69]憑這兩眼與百臂或千手不能防
[01:44.68]天闊闊雪漫漫共誰同航
[01:46.93]這沙滾滾水皺皺笑著浪蕩
[01:49.54]貪歡一刻偏教那女兒情長埋葬
[01:53.41]
[02:15.45]笑你我枉花光心計
[02:18.53]愛競逐鏡花那美麗
[02:21.14]怕幸運會轉眼遠逝
[02:23.76]為貪嗔喜惡怒著迷
[02:26.43]責你我太貪功戀勢
[02:28.98]怪大地眾生太美麗
[02:31.60]悔舊日太執信約誓
[02:34.26]為悲歡哀怨妒著迷
[02:36.90]啊 捨不得璀燦俗世
[02:42.04]啊 躲不開痴戀的欣慰
[02:47.34]啊 找不到色相代替
[02:52.52]啊 參一生參不透這條難題
[02:57.47]吞風吻雨葬落日未曾彷徨
[03:00.05]欺山趕海踐雪徑也未絕望
[03:02.64]拈花把酒偏折煞世人情狂
[03:05.27]憑這兩眼與百臂或千手不能防
[03:08.22]天闊闊雪漫漫共誰同航
[03:10.49]這沙滾滾水皺皺笑著浪蕩
[03:13.06]貪歡一刻偏教那女兒情長埋葬
[03:18.45]吞風吻雨葬落日未曾彷徨
[03:20.90]欺山趕海踐雪徑也未絕望
[03:23.54]拈花把酒偏折煞世人情狂
[03:26.21]憑這兩眼與百臂或千手不能防
[03:29.07]天闊闊雪漫漫共誰同航
[03:31.32]這沙滾滾水皺皺笑著浪蕩
[03:33.92]貪歡一刻偏教那女兒情長埋葬
[03:39.32]吞風吻雨葬落日未曾彷徨
[03:41.84]欺山趕海踐雪徑也未絕望
[03:44.38]拈花把酒偏折煞世人情狂
[03:47.04]憑這兩眼與百臂或千手不能防
[03:49.99]天闊闊雪漫漫共誰同航
[03:52.20]這沙滾滾水皺皺笑著浪蕩
[03:54.89]貪歡一刻偏教那女兒情長埋葬
[04:00.28]吞風吻雨葬落日未曾彷徨
[04:02.68]欺山趕海踐雪徑也未絕望
[04:05.25]拈花把酒偏折煞世人情狂
[04:07.90]憑這兩眼與百臂或千手不能防
[04:10.85]天闊闊雪漫漫共誰同航
[04:13.08]這沙滾滾水皺皺笑著浪蕩
[04:15.75]貪歡一刻偏教那女兒情長埋葬
[04:19.48]`;

 

2.index.js 主文件:

// 最開始獲取到的歌詞列表是字元串類型(不好操作)
let lrcArr = lrc.split('\n');
// 接收修正後的歌詞數組
let result = [];
// 獲取所要用到的dom列表
doms = {
    audio: document.querySelector("audio"),
    ul: document.querySelector("ul"),
    container: document.querySelector(".container")
}
// 將歌詞數組轉成由對象組成的數組,對象有time和word兩個屬性(為了方便操作)
for (let i = 0; i < lrcArr.length; i++) {
    var lrcData = lrcArr[i].split(']');
    var lrcTime = lrcData[0].substring(1);
    var obj = {
        time: parseTime(lrcTime),
        word: lrcData[1]
    }
    result.push(obj);
}
// 將tiem轉換為秒的形式
function parseTime(lrcTime) {
    lrcTimeArr = lrcTime.split(":")
    return +lrcTimeArr[0] * 60 + +lrcTimeArr[1];
}
// 獲取當前播放到的歌詞的下標
function getIndex() {
    let Time = doms.audio.currentTime;
    for (let i = 0; i < result.length; i++) {
        if (result[i].time > Time) {
            return i - 1;
        }
    }
}
// 創建歌詞列表
function createElements() {
    let frag = document.createDocumentFragment(); // 文檔片段
    for (let i = 0; i < result.length; i++) {
        let li = document.createElement("li");
        li.innerText = result[i].word;
        frag.appendChild(li);
    }
    doms.ul.appendChild(frag);
}
createElements();
// 獲取顯示視窗的可視高度
let containerHeight = doms.container.clientHeight;
// 獲取歌詞列表的可視高度
let liHeight = doms.ul.children[0].clientHeight;
// 設置最大最小偏移量,防止顯示效果不佳
let minOffset = 0;
let maxOffset = doms.ul.clientHeight - containerHeight;
// 控制歌詞滾動移動的函數
function setOffset() {
    let index = getIndex();
    // 計算滾動距離
    let offset = liHeight * index - containerHeight / 2 + liHeight / 2;
    if (offset < minOffset) {
        offset = minOffset;
    };
    if (offset > maxOffset) {
        offset = maxOffset;
    };
    // 滾動
    doms.ul.style.transform = `translateY(-${offset}px)`;
    // 清除之前的active
    let li = doms.ul.querySelector(".active")
    if (li) {
        li.classList.remove("active");
    }
    // 為當前所唱到的歌詞添加active
    li = doms.ul.children[index];
    if (li) {
        li.classList.add("active");
    }
};
// 當audio的播放時間更新時,觸發該事件
doms.audio.addEventListener("timeupdate", setOffset);

 

思路與解析:

首先,在寫js主文件之前,我們需要構思一下這個功能大概要怎樣去實現。

我看到這個功能的想法是 既然是要讓歌詞隨著播放進度而滾動,肯定最終是要監聽到當前播放的時間,去匹配當前播放時間所要播放的對應的歌詞,然後讓匹配到的歌詞移動到可視視窗的中央,再給它高亮啊什麼的。

有了怎麼實現,再去細分其中的細節步驟。

細節步驟:

<1>(1). 歌詞文件中的歌詞都是字元串類型,不好操作,我們需要將其轉換成數組,同時因為文件中的歌詞其實包含著兩個信息,‘每個歌詞的內容’ 和 ‘其對應的播放時間’,所以最好把每個歌詞又轉換成對象的形式加入數組,即:

編輯

 這種形式。

<1>(2). 歌詞文件中的歌詞是 [分:秒] 的格式,而最後要與audio的播放時間匹配的話,即audio.currentTime 這個時間是以秒為單位的,所以在<1>實現時,可以先封裝一個parseTime(轉換時間)的函數(這裡將轉換時間獨立封裝成函數,是為了代碼看起來更加簡潔清晰)。

<2>. 在把歌詞轉換為自己想要的格式後,有了每個歌詞的內容和其對應的時間,下一步要實現如何去匹配播放時間,然後獲取對應歌詞的下標(因為歌詞被轉換為數組),既然是獨立的功能,也可獨立封裝為getIndex函數。

<3>. 現在有了歌詞格式,有了獲取當前播放的對應歌詞下標的方法,我們可以將歌詞加入頁面了(不要忘記html中只寫了ul,為了能夠更靈活,所以html中並沒有直接寫死分配的歌詞),這個功能也可以封裝成一個createElements函數。

<4>. 之後就是封裝控制歌詞滾動的setOffset函數,當前播放的歌詞的下標,滾動就很容易實現了,因為每個歌詞li的高度都是一樣的,所以只需要用 每個li的下標*index + li高度的一半 - 可視視窗高度的一般即是ul需要移動的距離。

<5>. 最後就是給audio綁定一個監聽播放時間改變的事件就好了,回調函數直接用setOffset。

 

具體實現步驟:

<1>(1) 將歌詞轉換為所需的格式

// 最開始獲取到的歌詞列表是字元串類型(不好操作)
let lrcArr = lrc.split('\n');
// 接收修正後的歌詞數組
let result = [];
// 獲取所要用到的dom列表
doms = {
    audio: document.querySelector("audio"),
    ul: document.querySelector("ul"),
    container: document.querySelector(".container")
}
// 獲取所要用到的dom列表
doms = {
    audio: document.querySelector("audio"),
    ul: document.querySelector("ul"),
    container: document.querySelector(".container")
}
// 將歌詞數組轉成由對象組成的數組,對象有time和word兩個屬性(為了方便操作)
for (let i = 0; i < lrcArr.length; i++) {
    var lrcData = lrcArr[i].split(']');
    var lrcTime = lrcData[0].substring(1);
    var obj = {
        time: parseTime(lrcTime),
        word: lrcData[1]
    }
    result.push(obj);
}

【這裡將這次所要用到的dom元素,都放在了doms對象中,這樣更清晰】

用到的知識點:

1. split() 方法用於把一個字元串分割成字元串數組。

// 目的是為了把歌詞文件中,時間前後的[ ] 給去掉

2.string.substring(start, end) 截取字元串方法從 start 位置截取到 end 位置,end 可選

<1>(2). 轉換時間parseTime函數

// 將tiem轉換為秒的形式
function parseTime(lrcTime) {
    lrcTimeArr = lrcTime.split(":")
    return +lrcTimeArr[0] * 60 + +lrcTimeArr[1];
}

用到的知識點:

1.在字元串前加上+,可以將其轉換為數字

<2>. 獲取對應歌詞的下標:

// 獲取當前播放到的歌詞的下標

function getIndex() {

    let Time = doms.audio.currentTime;

    for (let i = 0; i < result.length; i++) {

        if (result[i].time > Time) {

            return i - 1;

        }

    }

}

這裡當匹配到歌詞數組中,匹配到的第一個播放時間大於當前播放時間的歌詞,它的前一個歌詞即為當前播放的歌詞,因為既然還沒到這一句,那就是前一句。

<3>. 創建歌詞li 

// 創建歌詞列表
function createElements() {
    let frag = document.createDocumentFragment(); // 文檔片段
    for (let i = 0; i < result.length; i++) {
        let li = document.createElement("li");
        li.innerText = result[i].word;
        frag.appendChild(li);
    }
    doms.ul.appendChild(frag);
}
createElements();

用到的知識點:

1.文檔片段 document.createDocumentFragment()  【但其實七十個不多】

        為了不頻繁的改動頁面的佈局導致reflow而影響效率,原先需要加入七十多次li,

        這裡可以統一先將li加入到文檔片段frag中,最後只需ul加入一次frag,即可完成。

 <4>. 滾動函數

// 獲取顯示視窗的可視高度
let containerHeight = doms.container.clientHeight;
// 獲取歌詞列表的可視高度
let liHeight = doms.ul.children[0].clientHeight;
// 設置最大最小偏移量,防止顯示效果不佳
let minOffset = 0;
let maxOffset = doms.ul.clientHeight - containerHeight;
// 控制歌詞滾動移動的函數
function setOffset() {
    let index = getIndex();
    // 計算滾動距離
    let offset = liHeight * index - containerHeight / 2 + liHeight / 2;
    if (offset < minOffset) {
        offset = minOffset;
    };
    if (offset > maxOffset) {
        offset = maxOffset;
    };
    // 滾動
    doms.ul.style.transform = `translateY(-${offset}px)`;
    // 清除之前的active
    let li = doms.ul.querySelector(".active")
    if (li) {
        li.classList.remove("active");
    }
    // 為當前所唱到的歌詞添加active
    li = doms.ul.children[index];
    if (li) {
        li.classList.add("active");
    }
};

要用到的知識點:

1. clientHeight 獲取可視高度

2.模板字元串(``)

3.利用transform:translateY();來進行滾動,不用margin-top原因也是因為會影響佈局導致reflow,影響效率。

<5>.給audio綁定播放時間更新事件:

// 當audio的播放時間更新時,觸發該事件
doms.audio.addEventListener("timeupdate", setOffset);

結語:

感謝觀看,文章主為記錄個人筆記方便以後重溫,希望能為各位解開一些疑惑


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 連接層 最上層是一些客戶端和鏈接服務,包含本地sock 通信和大多數基於客戶端/服務端工具實現的類似於 TCP/IP的通信。主要完成一些類似於連接處理、授權認證、及相關的安全方案。在該層上引入了線程 池的概念,為通過認證安全接入的客戶端提供線程。同樣在該層上可以實現基於SSL的安全鏈接。服務 器也會 ...
  • Whay need the CMake? 如果只是構建一個只有一個main.cpp的小型項目,那麼確實不需要CMake, 直接GCC、G++編譯,或者寫個build.sh腳本即可, 不需要把簡單的問題搞複雜化。 $ g++ main.cpp -o cmake_hello 但是如果你的項目分了很多模塊 ...
  • 使用記憶體對齊機制優化結構體性能,妙啊! 可以簡單理解為:將對齊繫數小的欄位,儘可能放在一起,儘量減少空白填充。 掌握了記憶體對齊機制後,結構體Struct的優化,調整下欄位順序,效果立竿見影。記憶體對齊其實就是典型的空間換時間的方式,來達到優化的目的。牢記對齊原則,對實際場景進行分析,減少空白填充。 ...
  • 昨天在群里看到有小伙伴問,Java里如何解析SQL語句然後格式化SQL,是否有現成類庫可以使用? 之前TJ沒有做過這類需求,所以去研究了一下,並找到了一個不過的解決方案,今天推薦給大家,如果您正要做類似內容,那就拿來試試,如果暫時沒需求,就先瞭解收藏(技多不壓身)。 JSqlParser JSqlP ...
  • 本文已經收錄到Github倉庫,該倉庫包含電腦基礎、Java基礎、多線程、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~ Github地址:https://github.c ...
  • 自動化測試環境的搭建 :一、安裝 selenium: 安裝方式一: pip install -U selenium 安裝方式二: 手動安裝 selenium: 1、安裝python包,選擇全部組件(pip、安裝過程中配置環境變數)解壓selenium-4.8.2.tar.gz,然後用cmd進入解壓目 ...
  • 代碼覆蓋率(Code coverage)是指在軟體測試中測試用例執行時覆蓋的代碼量與總代碼量的比例。代碼覆蓋率是軟體測試中一個重要的指標,它對於保障軟體質量、提高軟體可靠性和可維護性具有許多好處:發現代碼缺陷、提高代碼的可維護性、確保代碼的正確性和優化測試用例質量等。 我們常用的 IDE,Visua ...
  • Github Actions 是 Github 提供的一種持續集成(CI)和持續部署(CD)工具,可以自動化代碼開發、測試、構建和部署的過程。它可以在代碼倉庫中通過配置文件來定義工作流程(Workflow),包括觸發事件、執行任務和處理結果等。這些工作流程可以與Github倉庫的其他功能(如Issu ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...