Appearance
仿网易云-360度混响
一直在用网易云音乐听歌,感觉他的这个动效还是挺不错的,最近也是想试试canvas绘图相关的。尝试了几次之后感觉效果还不错,不过距离网易云的还是有些差距。
本期准备仿照制作如下效果:
偷偷使用最近比较流行的罗刹海市的音乐来展示这个效果。
效果展示如下:
具体的流程大体上就是获取音频数据,然后根据音频数据绘制在canvas上,不同的绘制方式就能有很多惊艳的效果,毕竟还是数学最令人着迷了。
提取音频数据
普通的audio标签无法提取音频数据,查了一下需要使用到 AudioContext 这个浏览器内置对象。 使用来说也比较简单,大体上就是创建,加载音频,播放等几个环境,在播放的时候通过链接音频处理节点获取音频数据。
几个比较重要的点如下:
javascript
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioCtx = new AudioContext()
// 创建播放节点
this.bufferSourceNode = this.audioCtx.createBufferSource()
this.bufferSourceNode.connect(this.audioCtx.destination)
// 创建分析器,从这个分析器中能够得到频域跟时域的数据,不过频域的数据分散不够均匀,感觉还是时域的展示效果好一些
this.analyser = this.audioCtx.createAnalyser()
this.analyser.fftSize = this.sampleRate
this.analyser.smoothingTimeConstant = 1
this.player.bufferSourceNode.connect(this.analyser)
this.analyser.getByteTimeDomainData(data)
剩下的就是把这些内容给组织起来了。
javascript
class Player {
constructor(canvasName) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioCtx = new AudioContext()
this.bufferSourceNode = this.audioCtx.createBufferSource()
this.canvas = document.getElementById(canvasName)
this.audioBuffer = null
this.initEffect()
}
stop() {
this.audioCtx.suspend()
}
resume() {
this.audioCtx.resume()
}
initEffect() {
this.bufferSourceNode = this.audioCtx.createBufferSource()
this.bufferSourceNode.connect(this.audioCtx.destination)
// 显示效果,可以随时替换
this.effect = new DefaultEffect(this)
}
play(url) {
let that = this
if (that.audioBuffer) {
that.initEffect()
}
fetch(url, {
method: 'get',
responseType: 'arraybuffer'
}).then(res => {
return res.arrayBuffer();
}).then(arraybuffer => {
that.audioCtx.decodeAudioData(arraybuffer, function(buffer) {
that.audioBuffer = buffer
that.bufferSourceNode.buffer = buffer
that.bufferSourceNode.start(0)
});
})
}
}
360混响效果
看画面他像从圆心发射的线条,只不过用一个内圆把中间给盖上了。 那么接下来就是先画一个放射线,只需要按照均衡的角度来画一下就完事了。 本例子这个效果把园分了128条线,并且把其实坐标给从圆心偏移到内圆的边上。 canvas画线的API如下:
javacript
let jiaodu = i * 360 / count
let sx = Math.sin(jiaodu * Math.PI / 180) * minRadius
let sy = Math.cos(jiaodu * Math.PI / 180) * minRadius
let d = Math.max(0, this.lastData[i] - 127)
// let d = data[i] - 90
let endRatio = (minRadius + (d / 128) * 100) / minRadius
let ex = Math.sin(jiaodu * Math.PI / 180) * minRadius * endRatio
let ey = Math.cos(jiaodu * Math.PI / 180) * minRadius * endRatio
sx += centerX
sy += centerY
ex += centerX
ey += centerY
this.ctx.beginPath();
this.ctx.moveTo(ex, ey); // 起点
this.ctx.lineWidth = 4;
this.ctx.lineCap = "round"; // 圆角,看起来更好看一点
this.ctx.lineTo(lex, ley); // 终点
this.ctx.strokeStyle = 'rgba(255,0,0,0.1)'; // 颜色
this.ctx.stroke();
效果如下:
为了让这个更好看一点,看到好像能够在这个线的外层再画一层,只不过颜色看起来比较浅。
另外接上时域数据之后,发现跳动比较大,然后加了一层缓存,减缓曲线的回落速度。
javascript
for(let i = 0; i < this.sampleRate; i++) {
if (this.lastData[i]> 8) this.lastData[i] -= 8
this.lastData[i] = Math.max(this.lastData[i], data[i])
}
整体效果代码如下:
javascript
class DefaultEffect {
constructor(player) {
this.player = player
this.width = player.canvas.width
this.height = player.canvas.height
this.ctx = player.canvas.getContext('2d')
this.sampleRate = 128
this.audioCtx = player.audioCtx
this.lastData = new Uint8Array(this.sampleRate)
this.analyser = this.audioCtx.createAnalyser()
this.analyser.fftSize = this.sampleRate
this.analyser.smoothingTimeConstant = 1
this.player.bufferSourceNode.connect(this.analyser)
}
// 当暂停的时候使用这个输出默认的效果,是动画不那么呆板
idleData(delta) {
let data = []
for(let i = 0; i < this.sampleRate; i++) {
data.push((Math.sin(i + delta/1000) + 1) * 20 + 127)
}
return data
}
getData(delta) {
var data = new Uint8Array(this.sampleRate)
if (this.audioCtx) {
if (this.audioCtx.state == 'running') {
this.analyser.getByteTimeDomainData(data)
} else {
data = this.idleData(delta)
}
} else {
data = this.idleData(delta)
}
return data
}
draw(delta) {
this.ctx.clearRect(0,0,this.width,this.height)
let data = this.getData(delta)
for(let i = 0; i < this.sampleRate; i++) {
if (this.lastData[i]> 8) this.lastData[i] -= 8
this.lastData[i] = Math.max(this.lastData[i], data[i])
}
let centerX = this.width/2
let centerY = this.height/2
let minRadius = 150
let count = 128
for(let i = 0; i < count; i++) {
let jiaodu = i * 360 / count
let sx = Math.sin(jiaodu * Math.PI / 180) * minRadius
let sy = Math.cos(jiaodu * Math.PI / 180) * minRadius
let d = Math.max(0, this.lastData[i] - 127)
// let d = data[i] - 90
let endRatio = (minRadius + (d / 128) * 100) / minRadius
let ex = Math.sin(jiaodu * Math.PI / 180) * minRadius * endRatio
let ey = Math.cos(jiaodu * Math.PI / 180) * minRadius * endRatio
sx += centerX
sy += centerY
ex += centerX
ey += centerY
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineWidth = 4;
this.ctx.lineCap = "round";
this.ctx.lineTo(ex, ey);
this.ctx.strokeStyle = 'red';
this.ctx.stroke();
let lex = Math.sin(jiaodu * Math.PI / 180) * (minRadius * endRatio + 30)
let ley = Math.cos(jiaodu * Math.PI / 180) * (minRadius * endRatio + 30)
lex += centerX
ley += centerY
this.ctx.beginPath();
this.ctx.moveTo(ex, ey);
this.ctx.lineWidth = 4;
this.ctx.lineCap = "round";
this.ctx.lineTo(lex, ley);
this.ctx.strokeStyle = 'rgba(255,0,0,0.1)';
this.ctx.stroke();
}
}
}
使用的时候
初始化一个player,然后定时刷新canvas以显示效果。
javascript
var player;
var effectName;
onMounted( () => {
player = new Player('myCanvas', effectName)
setInterval(() => {
if (player) {
requestAnimationFrame(player.effect.draw.bind(player.effect))
}
}, 50);
player.play(url)
})