这不是打算用于本站的东西(如果有需要,我会直接修改aplayer的样式),只是一个小练习

很简单的小练习,请各位大佬海涵

效果

当音乐播放到某一特定位置时,歌词出现滚动高亮效果

样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
* {
margin: 0;
padding: 0;
}

.container {
width: 100vw;
height: 100vh;
background-color: #222;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
box-sizing: border-box;
overflow: hidden;
}

.player {
display: flex;
justify-content: center;
padding: 20px;
}

.player audio {
width: 50vw;
}

.lyric {
width: 100%;
height: 100%;
color: #888;
overflow: hidden;
}

.lyric ul {
padding: 20px;
transform: translateY(0px);
transition: 0.2s;
}

.lyric li {
list-style: none;
text-align: center;
white-space: nowrap;
overflow: hidden;
height: 30px;
line-height: 30px;
transition: 0.2s;
}

.lyric li.active {
color: white;
transform: scale(1.25);
}

结构与行为

测试数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default `[00:13.445]Digital affection
[00:17.693]Mechanical perfection
[00:20.688]I was made for you
[00:26.691]Outer world dimension
[00:30.185]Hardwired connection
[00:38.124]We can have it all tonight
[00:38.623]
[00:39.122]Nothing matters when I'm with you baby
[00:41.876]Plug me in like your electric lady
[00:45.825]Nothing matters when I’m with you baby
[00:47.833]Plug me in like your electric lady
[01:01.324]Let me in like you electric baby
[01:19.074]
[01:19.331]Electric
[01:23.825]Nothing matters when I'm with you baby
[01:26.574]Let me in like you electric baby
[01:29.581]
[01:40.333]Let me in like you electric baby
[01:55.833]
[01:56.331]Your electric
[02:02.577]Nothing matters when I'm with you baby
[02:05.073]Let me in like you electric baby`

这是我从网易云音乐弄来的,这个平台的歌词就是这种格式,不知道这是不是通用的

编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<!DOCTYPE html>
<html lang="en">

<head>
<title>Document</title>
<link rel="stylesheet" href="./css/index.css">
</head>

<body>
<div class="container">
<div class="player">
<audio controls src="./assets/Electric Lady (feat. XO ELIZA).flac"></audio>
</div>
<div class="lyric">
<ul>
</ul>
</div>

</div>

<script type="module">
import lrcStr from './js/data.js'

/**
* 元素
*/
const DOMs = {
lyricContainer: document.querySelector('.lyric'),
lyricUl: document.querySelector('ul'),
audio: document.querySelector('audio')
}

/**
* 数据处理
*/
// #region
/**
* 解析歌词字符串,得到歌词对象数组
* @param {string} lrcStr 歌词字符串
* @return {[{time: number, words: string}]} 歌词时间,歌词内容
*/
const parseLrc = (lrcStr) => {
return lrcStr.split('\n').map(l => {
let [time, words] = l.split(']')
time = parseTime(time.substring(1))
return { time, words }
})
}

/**
* 解析时间字符串,得到时间(毫秒)
* @param {string} timeStr 时间字符串
* @return {number} 时间
*/
const parseTime = (timeStr) => {
const [minute, second] = timeStr.split(':')
return minute * 60 + +second
}

/**
* 计算当前应高亮显示的歌词
* @param {[{time: number, words: string}]} lyric
* @return {number} 需高亮的歌词坐标,若无任何歌词需显示则返回-1
*/
const findIndex = (lyric) => {
const currentTime = DOMs.audio.currentTime
for (let i = 0; i < lyric.length; i++) {
if (currentTime < lyric[i].time) {
return i - 1
}
}
return lyric.length - 1
}
// #endregion

/**
* 页面展现
*/
// #region
// 歌词信息数组
const lyric = parseLrc(lrcStr)
/**
* 创建歌词元素
*/
const init = () => {
const frag = document.createDocumentFragment()

lyric.forEach(l => {
const li = document.createElement('li')
li.textContent = l.words
li.dataset.time = l.time
frag.appendChild(li)
})

DOMs.lyricUl.appendChild(frag)
}
init()
// #endregion

/**
* 事件处理
*/
// #region
// 歌词容器高度
let containerHeight
// 所有歌词高度
let lrcUlHeight
// 所有歌词上内边距
let lrcUlPaddingT
// 所有歌词最大偏移量
let maxUlOffset
// 每句歌词高度
let lrcLiHeight

// 计算所需属性
const computeData = () => {
containerHeight = DOMs.lyricContainer.clientHeight
lrcUlHeight = DOMs.lyricUl.clientHeight
lrcUlPaddingT = Number(getComputedStyle(DOMs.lyricUl)['padding-top'].split('p')[0])
maxUlOffset = lrcUlHeight - containerHeight < 0 ? 0 : lrcUlHeight - containerHeight
lrcLiHeight = DOMs.lyricUl.children[0].clientHeight
}

/**
* 监听容器高度变化
*/
const observer = new ResizeObserver(computeData)
observer.observe(DOMs.lyricContainer, {
box: 'border-box'
})

// 初始化数值
computeData()
/**
* 设置偏移量
*/
const setOffset = () => {
// 高亮歌词
const index = findIndex(lyric)
// 歌词所在高度
const lrcHeight = lrcLiHeight * index + lrcLiHeight / 2

// ul偏移量
let ulOffset = lrcUlPaddingT + lrcHeight - containerHeight / 2
// 边界控制
if (ulOffset < 0) ulOffset = 0
if (ulOffset > maxUlOffset) ulOffset = maxUlOffset

// 进行偏移
DOMs.lyricUl.style.transform = `translateY(-${ulOffset}px)`

// 去除旧高亮
const oldLi = DOMs.lyricUl.querySelector('.active')
if (oldLi) oldLi.classList.remove('active')

// 添加新高亮
const newLi = DOMs.lyricUl.children[index]
if (newLi) newLi.classList.add('active')
}

/**
* 绑定播放时间改变事件
*/
DOMs.audio.addEventListener('timeupdate', setOffset)
// #endregion
</script>
</body>

</html>

因为要确保高亮歌词显示在中心(除非它位于开头或结尾),所以需要监听歌词容器的高度,每当它发生变化时,需要重新计算这些与偏移量相关的数值

ResizeObserver类似于MutationObserver,但它专门监听元素的几何尺寸,主流电脑端浏览器都已兼容

发散

如果需要允许用户滚动歌词怎么办?

那就把偏移量实现换成滚动条实现,应该是这样

但用户在浏览所有歌词时,由于播放时间改变事件持续触发,很有可能出现内容动不动就滑走的情况

所以我认为可能需要一个计时器控制flag,这个flag应该代表用户是否有在浏览歌词,正在浏览时则不允许播放时间改变事件滑动歌词

用户滑动歌词就相当于正在浏览,若用户若干秒内没有再进行滑动,则相当于用户并未在浏览