实现效果
实现分析
解析歌词字符串转化为对象
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
向上偏移的位置以及当前高亮歌词
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();
Comment here is closed