带进度条+歌词同步的canvas画布音频播放器
带进度条+歌词同步的canvas画布音频播放器 | 马黑黑做童安格的《耶利亚女郎》帖子时,一时兴起用canvas画布做了一个进度条加歌词同步的音频播放器,感觉基本功能、外观都还可以,就是可定制性不够强。想想还是认为将播放器作为一个实例对象加以操作,使用时方能拥有更方便的自定义设定。
实例化对象适合做音频播放器,属性、方法可以无序化存在,需要什么不需要什么可以随意增删,无需因为一个属性或方法的变化四处去找寻和修改关联的代码语句,基于对象的变动所涉及修改范围基本就在对象代码块之内。
所以,建立了一个 mplayer 对象,它是实例化的,因为无需克隆:
let mplayer = {
属性1: 值,
属性2: 值,
属性N: 值,
方法1: function() { ... },
方法2: function() { ... },
方法M: function() { ... },
}
每一个属性和方法都是以键值对的形式存在,每一个属性和方法之间用逗号隔开。分行不是必须的,分行只是为了方便组织和阅读代码。
我们的 mplayer 对象的最终任务是描述播放器界面及其行为。实现手段是,对象给出各种组成播放器所需的“零件”即属性并提供这些零件的组装方法。比方说,播放器的按钮是个圆形的小东东,它上面还有一个表示播放的三角形图案或是表示暂停的两道杠,圆形、三角形、两道杠这些部件,通过对象的方法实现,而圆形等的尺寸、位置以及颜色啥的,是对象提供的“零件”即属性。具体说,圆形的按钮,圆心xy坐标及其半径,还有它要上什么颜色,都是 mplayer 对象以键值对的形式一一提供的属性,而圆形按钮画在哪、怎么画,则是通过 mplayer 对象提供的方法去完成。mplayer 提供的所有属性与方法,都是基于一个播放器的基本样式和功能,换言之,我们把实现一个播放器所需的“零件”和“组装”方法全部封装在了一个叫做 mplayer 的对象里。
对象的方法其实就是函数,函数是可以接收“第三方”提供的参数的。例如,mplayer 要绘制出歌词,它并不知道歌词是什么,这时,就由 audio 控件的播放进度机制来处理歌词,并一句一句地传递给 mplayer 对象,mplayer 则将歌词给绘制出来。就是说,针对lrc同步,mplayer 只管处理一个歌词变量 text,text 的内容传递过来了它就绘制这些文本。
具体实现过程请在播放器源码中体会:
<canvas id="player" style="position: absolute; left: 10px; top: 10px; border: 1px solid;"></canvas>
<script>
let ctx = player.getContext('2d'),
w = player.width = 350,
h = player.height = 100,
flag, //鼠标滑过按钮标识
aud = new Audio();
let mplayer = {
startX: 30, //圆形按钮圆心x
startY: 70, //圆形按钮圆心y
radius: 16, //圆形按钮半径
c_lrc: 'teal', //lrc歌词颜色
c_time: 'teal', //时间信息颜色
c_track: '#eee', //进度轨线颜色
c_prg: 'red', //进度条颜色
c_circle: 'lightblue', //圆圈颜色
c_btn: 'snow', //按钮标识颜色
c_btnhover: 'pink', //按钮标识鼠标滑过颜色
lineLen: 200, //进度条长度
drawLrc: function(text) { //lrc同步
ctx.clearRect(0, 0, w, 50);
ctx.fillStyle = this.c_lrc;
ctx.textAlign = 'center';
ctx.beginPath();
ctx.font = '1.2em sans-serif';
ctx.fillText(text, w/2, 30);
ctx.fill();
},
drawProgress: function(prog, text) { //进度
ctx.clearRect(this.startX + this.radius, this.startY - 10, w - this.startX + this.radius, 40);
ctx.beginPath();
ctx.font = '14px sans-serif';
ctx.textBaseline = 'middle';
ctx.strokeStyle = this.c_track;
ctx.moveTo(this.startX + this.radius + 4, this.startY);
ctx.lineTo(this.startX + this.radius + 4 + this.lineLen, this.startY); //底线
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = this.c_prg;
ctx.moveTo(this.startX + this.radius + 4, this.startY);
ctx.lineTo(this.startX + this.radius + 4 + prog, this.startY); //进度线
ctx.stroke();
ctx.fillStyle = this.c_time;
ctx.textAlign = 'left';
ctx.fillText(text, this.startX + this.radius + 8 + this.lineLen, this.startY); //数字进度
ctx.fill();
},
drawBtn: function(id) { //绘制播放+暂停按钮
ctx.clearRect(this.startX - this.radius, this.startY - this.radius, this.radius *2, this.radius*2);
ctx.fillStyle = this.c_circle;
ctx.beginPath();
ctx.arc(this.startX, this.startY, this.radius, 0, 2*Math.PI);
ctx.fill();
ctx.fillStyle = flag ? this.c_btnhover : this.c_btn;
ctx.beginPath();
if (id) { //播放图标
ctx.moveTo(this.startX - this.radius / 2 + 1, this.startY - this.radius / 2);
ctx.lineTo(this.startX - this.radius / 2 + 1, this.startY + this.radius / 2);
ctx.lineTo(this.startX + this.radius / 2 + 1, this.startY);
ctx.fill();
} else { //暂停图标
ctx.fillRect(this.startX - this.radius / 2 + 5, this.startY - this.radius / 2, 2, this.radius);
ctx.fillRect(this.startX - this.radius / 2 + 10, this.startY - this.radius / 2, 2, this.radius);
}
},
isHover: function (x, y) {
return Math.pow(x - this.startX, 2) + Math.pow(y - this.startY, 2) <= Math.pow(this.radius, 2);
},
}
let lrcAr = [
['0.01','童安格耶利亚女郎'],
['34.10','很远的地方有个女郎名字叫做耶利亚'],
['42.33','有人在传说她的眼睛看了使人更年轻'],
['50.62','如果你得到她的拥抱你就永远不会老'],
['59.15','为了这个神奇的传说我要努力去寻找'],
['66.42','耶利亚神秘耶利亚耶利耶利亚'],
['74.77','耶利亚神秘耶利亚我一定要找到她'],
['101.12','很远的地方有个女郎名字叫做耶利亚'],
['109.32','有人在传说她的眼睛看了使人更年轻'],
['117.90','如果你得到她的拥抱你就永远不会老'],
['126.22','为了这个神奇的传说我要努力去寻找'],
['133.57','耶利亚神秘耶利亚耶利耶利亚'],
['141.90','耶利亚神秘耶利亚我一定要找到她'],
['150.29','耶利亚神秘耶利亚耶利耶利亚'],
['158.76','耶利亚神秘耶利亚我一定要找到她'],
['198.70','耶利亚神秘耶利亚耶利耶利亚'],
['207.11','耶利亚神秘耶利亚我一定要找到她'],
['215.28','耶利亚神秘耶利亚耶利耶利亚'],
['223.72','耶利亚神秘耶利亚我一定要找到她']
];
//audio控件设置
aud.src = '耶利亚女郎.mp3';
aud.autoplay = true;
aud.loop = true;
//初始化按钮
mplayer.drawBtn(aud.paused);
mplayer.drawProgress(1, '00:00 | 00:00');
mplayer.drawLrc('等待播放 ...');
//监听鼠标经过
player.addEventListener('mousemove', (e) => {
flag = mplayer.isHover(e.offsetX, e.offsetY);
if (flag) {
player.style.cursor = 'pointer';
mplayer.drawBtn(aud.paused);
} else {
player.style.cursor = 'default';
mplayer.drawBtn(aud.paused);
}
});
player.addEventListener('click', (e) => { if(flag) aud.paused ? aud.play() : aud.pause(); }); //画布点击事件
aud.addEventListener('playing', ()=> mplayer.drawBtn(false)); //监听播放状态
aud.addEventListener('pause', ()=> mplayer.drawBtn(true)); //监听暂停状态
//进度监听
aud.addEventListener('timeupdate', () => {
let prg = mplayer.lineLen * aud.currentTime / aud.duration, tMsg = toMin(aud.duration) + ' | ' + toMin(aud.currentTime);
mplayer.drawProgress(prg, tMsg);
for(j = 0; j < lrcAr.length;j ++) {
if(aud.currentTime >= lrcAr) mplayer.drawLrc(lrcAr);
}
});
//格式化时间信息 → 00:00
function toMin(val) {
if (!val) return '00:00';
val = Math.floor(val);
let min = parseInt(val / 60);
let sec = parseFloat(val % 60);
if(min < 10) min = '0' + min;
if(sec < 10) sec = '0' + sec;
return min + ':' + sec;
}
</script>播放器演示在二楼。
本帖最后由 马黑黑 于 2022-8-26 12:57 编辑 <br /><br /><canvas id="player" style="border: 1px solid;"></canvas>
<script>
let ctx = player.getContext('2d'),
w = player.width = 350,
h = player.height = 100,
flag,
aud = new Audio();
let mplayer = {
startX: 30,
startY: 70,
radius: 16,
c_lrc: 'teal',
c_time: 'teal',
c_track: '#eee',
c_prg: 'red',
c_circle: 'lightblue',
c_btn: 'snow',
c_btnhover: 'pink',
lineLen: 200,
drawLrc: function(text) {
ctx.clearRect(0, 0, w, 50);
ctx.fillStyle = this.c_lrc;
ctx.textAlign = 'center';
ctx.beginPath();
ctx.font = '1.2em sans-serif';
ctx.fillText(text, w/2, 30);
ctx.fill();
},
drawProgress: function(prog, text) {
ctx.clearRect(this.startX + this.radius, this.startY - 10, w - this.startX + this.radius, 40);
ctx.beginPath();
ctx.font = '14px sans-serif';
ctx.textBaseline = 'middle';
ctx.strokeStyle = this.c_track;
ctx.moveTo(this.startX + this.radius + 4, this.startY);
ctx.lineTo(this.startX + this.radius + 4 + this.lineLen, this.startY);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = this.c_prg;
ctx.moveTo(this.startX + this.radius + 4, this.startY);
ctx.lineTo(this.startX + this.radius + 4 + prog, this.startY);
ctx.stroke();
ctx.fillStyle = this.c_time;
ctx.textAlign = 'left';
ctx.fillText(text, this.startX + this.radius + 8 + this.lineLen, this.startY);
ctx.fill();
},
drawBtn: function(id) { //绘制播放+暂停按钮
ctx.clearRect(this.startX - this.radius, this.startY - this.radius, this.radius *2, this.radius*2);
ctx.fillStyle = this.c_circle;
ctx.beginPath();
ctx.arc(this.startX, this.startY, this.radius, 0, 2*Math.PI);
ctx.fill();
ctx.fillStyle = flag ? this.c_btnhover : this.c_btn;
ctx.beginPath();
if (id) {
ctx.moveTo(this.startX - this.radius / 2 + 1, this.startY - this.radius / 2);
ctx.lineTo(this.startX - this.radius / 2 + 1, this.startY + this.radius / 2);
ctx.lineTo(this.startX + this.radius / 2 + 1, this.startY);
ctx.fill();
} else {
ctx.fillRect(this.startX - this.radius / 2 + 5, this.startY - this.radius / 2, 2, this.radius);
ctx.fillRect(this.startX - this.radius / 2 + 10, this.startY - this.radius / 2, 2, this.radius);
}
},
isHover: function (x, y) {
return Math.pow(x - this.startX, 2) + Math.pow(y - this.startY, 2) <= Math.pow(this.radius, 2);
},
}
let lrcAr = [
['0.01','童安格耶利亚女郎'],
['34.10','很远的地方有个女郎名字叫做耶利亚'],
['42.33','有人在传说她的眼睛看了使人更年轻'],
['50.62','如果你得到她的拥抱你就永远不会老'],
['59.15','为了这个神奇的传说我要努力去寻找'],
['66.42','耶利亚神秘耶利亚耶利耶利亚'],
['74.77','耶利亚神秘耶利亚我一定要找到她'],
['101.12','很远的地方有个女郎名字叫做耶利亚'],
['109.32','有人在传说她的眼睛看了使人更年轻'],
['117.90','如果你得到她的拥抱你就永远不会老'],
['126.22','为了这个神奇的传说我要努力去寻找'],
['133.57','耶利亚神秘耶利亚耶利耶利亚'],
['141.90','耶利亚神秘耶利亚我一定要找到她'],
['150.29','耶利亚神秘耶利亚耶利耶利亚'],
['158.76','耶利亚神秘耶利亚我一定要找到她'],
['198.70','耶利亚神秘耶利亚耶利耶利亚'],
['207.11','耶利亚神秘耶利亚我一定要找到她'],
['215.28','耶利亚神秘耶利亚耶利耶利亚'],
['223.72','耶利亚神秘耶利亚我一定要找到她']
];
aud.src = 'https://music.163.com/song/media/outer/url?id=150852.mp3';
aud.autoplay = true;
aud.loop = true;
mplayer.drawBtn(aud.paused);
mplayer.drawProgress(1, '00:00 | 00:00');
mplayer.drawLrc('等待播放 ...');
player.addEventListener('mousemove', (e) => {
flag = mplayer.isHover(e.offsetX, e.offsetY);
if (flag) {
player.style.cursor = 'pointer';
mplayer.drawBtn(aud.paused);
} else {
player.style.cursor = 'default';
mplayer.drawBtn(aud.paused);
}
});
player.addEventListener('click', (e) => { if(flag) aud.paused ? aud.play() : aud.pause(); });
aud.addEventListener('playing', ()=> mplayer.drawBtn(false));
aud.addEventListener('pause', ()=> mplayer.drawBtn(true));
aud.addEventListener('timeupdate', () => {
let prg = mplayer.lineLen * aud.currentTime / aud.duration, tMsg = toMin(aud.duration) + ' | ' + toMin(aud.currentTime);
mplayer.drawProgress(prg, tMsg);
for(j = 0; j < lrcAr.length;j ++) {
if(aud.currentTime >= lrcAr) mplayer.drawLrc(lrcAr);
}
});
function toMin(val) {
if (!val) return '00:00';
val = Math.floor(val);
let min = parseInt(val / 60);
let sec = parseFloat(val % 60);
if(min < 10) min = '0' + min;
if(sec < 10) sec = '0' + sec;
return min + ':' + sec;
}
</script> 播放器总共有四个地方需要反复重绘:
一、lrc歌词
由audio控件的timeupdate监听事件驱动,每一句歌词,按currentTime步进刷新,每次刷新先区域性擦除再重绘。
二和三、进度条和播放数字信息
这两样何在一处处理,也是由 timeupdate 事件驱动,每次刷新所擦除的区域从圆形按钮之后到画布的最右边,擦除区域的高度在行内。
四、播放暂停按钮
有鼠标指针在画布上的移动事件驱动,每次刷新的擦除区域圆形自身区域。 黒黑这个是播放器分享啊{:4_178:} 今天周末我可以玩的,黑黑辛苦又分享新的教程了{:4_199:} 这个好,把别的都拿掉了,单纯讲画布播放器。这个真不容易,所有的东东都是画出来的,厉害{:4_187:} 小辣椒 发表于 2022-8-26 15:01
黒黑这个是播放器分享啊
是的 红影 发表于 2022-8-26 17:10
这个好,把别的都拿掉了,单纯讲画布播放器。这个真不容易,所有的东东都是画出来的,厉害
画布是一个不同寻常的画家{:4_170:} 小辣椒 发表于 2022-8-26 15:03
今天周末我可以玩的,黑黑辛苦又分享新的教程了
过一下子还有一个,在现在这个的基础上加入可控进度功能 马黑黑 发表于 2022-8-26 19:22
画布是一个不同寻常的画家
黑黑才是最出类拔萃的画家呢{:4_187:} 红影 发表于 2022-8-26 20:19
黑黑才是最出类拔萃的画家呢
我画盲 马黑黑 发表于 2022-8-26 19:22
过一下子还有一个,在现在这个的基础上加入可控进度功能
是今天吗 小辣椒 发表于 2022-8-26 20:46
是今天吗
{:4_181:} 马黑黑 发表于 2022-8-26 20:20
我画盲
你可以用代码画的呢,在画布上描绘所有的梦{:4_187:} 红影 发表于 2022-8-27 19:38
你可以用代码画的呢,在画布上描绘所有的梦
没那么夸张 马黑黑 发表于 2022-8-27 20:20
没那么夸张
反正我在心目中你无所不能{:4_187:} 红影 发表于 2022-8-27 20:49
反正我在心目中你无所不能
这是少见多怪的原因 马黑黑 发表于 2022-8-27 21:15
这是少见多怪的原因
论坛里就你一个会代码的,你是孤勇者{:4_199:} 红影 发表于 2022-8-28 11:02
论坛里就你一个会代码的,你是孤勇者
不是的。会代码的人,根据观察,有一些的 马黑黑 发表于 2022-8-28 11:07
不是的。会代码的人,根据观察,有一些的
哦哦,反正我知道的就你一个{:4_173:}
页:
[1]
2