纯动画效果css
带音乐特效,仅支持谷歌浏览器html
代码在此git
这个动画效果是在网易云音乐 app 上看到的,抱着复习 canvas 的心态来实现。github
如今开始分析而且实现所须要的功能canvas
根据效果,咱们能初步想到的须要设置的参数以下,参数的具体值能够在随后实现中进一步修改数组
const originParams = {
cover: '', // 中心的封面图
size: 500, // 画布 canvas 的尺寸
radius: 100, // 封面图,中心圆的半径,小于零则为容器的百分比
interval: [500, 1500], // 涟漪出现的最小频率(毫秒)
centerColor: '#ddd', // 封面图位置的颜色(在没有封面图时显示)
borderWidth: 5, // 封面图边框的宽度
borderColor: '#aaa', // 封面图边框的颜色
rippleWidth: 4, // 涟漪圆环的宽度
rippleColor: '#fff', // 涟漪颜色
pointRadius: 8, // 涟漪圆点的半径
rotateAngle: .3, // 封面图每帧旋转的角度
}
复制代码
咱们知道,动画的原理,就是趁脑子不注意反应不过来的时候偷偷换上一副差很少的画面,这样一步一步的替换,就造成了动画,而这一步一步就能够叫作帧
。浏览器
因此,在渲染圆圈及圆点时,咱们须要一个数组来存储他们每一次渲染的位置。app
另外,咱们须要一些必要的初始化判断,以及声明一些公共的参数dom
综上,咱们基本能够写出以下的构造函数ide
class Ripple {
constructor(container, params = {}) {
const originParams = {
cover: '',
size: 500,
radius: 100,
interval: [500, 1500],
centerColor: '#ddd',
borderWidth: 5,
borderColor: '#aaa',
rippleWidth: 4,
rippleColor: '#fff',
pointRadius: 8,
rotateAngle: .3,
}
this.container = typeof container === "string" ? document.querySelector(container) : container
this.params = Object.assign(originParams, params)
this.cover = this.params.cover
this.radius = this.params.radius < 1 ? this.params.size * this.params.radius : this.params.radius
this.center = this.params.size / 2 // 中心点
this.rate = 0 // 记录播放的帧数
this.frame = null // 帧动画,用于取消
this.rippleLines = [] // 存储涟漪圆环的半径
this.ripplePoints = [] // 存储涟漪点距离中心点的距离
}
}
复制代码
canvas 中图片渲染且旋转并不容易,因此在 cover
参数传值时,经过 img
标签来渲染。
另外的,咱们须要一些其余必要的 CSS 添加在元素上
class Ripple{
initCanvas() {
this.container.innerHTML = `<canvas width="${this.params.size}" height="${this.params.size}"></canvas>${this.cover ? `<img src="${this.cover}" alt="">` : ''}`
this.cover = this.container.querySelector('img')
this.canvas = this.container.querySelector('canvas')
this.ctx = this.canvas.getContext('2d')
this.rotate = 0
const containerStyle = { ... }
const canvasStyle = { ... }
const coverStyle = { ... }
utils.addStyles(this.container, containerStyle)
utils.addStyles(this.canvas, canvasStyle)
utils.addStyles(this.cover, coverStyle)
this.strokeBorder()
}
}
复制代码
canvas 的基本用法,就很少赘述了
class Ripple{
strokeCenterCircle() {
const ctx = this.ctx
ctx.beginPath()
ctx.arc(this.center, this.center, this.radius, 0, 2 * Math.PI)
ctx.closePath()
ctx.fillStyle = this.params.centerColor
ctx.fill()
}
strokeBorder() {
const ctx = this.ctx
ctx.beginPath()
ctx.arc(this.center, this.center, this.radius + this.params.borderWidth / 2, 0, 2 * Math.PI)
ctx.closePath()
ctx.strokeStyle = this.params.borderColor
ctx.lineWidth = 5
ctx.stroke()
}
}
复制代码
这不就是一个圈加一个圆吗
class Ripple{
drawRipple() {
const ctx = this.ctx
// 画外圈
ctx.beginPath()
ctx.arc(this.center, this.center, 200, 0, Math.PI * 2)
ctx.strokeStyle = 'rgba(255,255,255,0.4)'
ctx.lineWidth = this.params.rippleWidth
ctx.stroke()
// 画点
ctx.beginPath()
ctx.arc(this.center - 200/Math.sqrt(2), this.center - 200/Math.sqrt(2), this.params.pointRadius, 0, 2 * Math.PI)
ctx.closePath()
ctx.fillStyle = 'rgba(255,255,255,0.4)'
ctx.fill()
}
}
复制代码
因而出现了下面的问题
出现的缘由也很简单,两个半透明的图形的重合部分透明度确定是会加剧的,因此只能经过画一个不完整的圆圈(正好把圆点的部分隔过去)来解决了,解决方式以下图:
为了容易看到,连线略微向内移动了一点,因此咱们的问题就是已知 r、R,求角度 θ,解答就不作详解啦,算是高中数学的应用。咱们能够获得角度为 Math.asin(R / r / 2) * 4
。
圆环和圆点的重合已经解决了,如今须要的是在每次刷新时更新他们的位置,若是达到了条件,则须要新添加一个圆环和点,若是圆环的半径超出了画布,则删掉对应的数据
class Ripple{
strokeRipple() {
// 当圆环大小超出画布时,删除改圆环数据
if (this.rippleLines[0] > this.params.size) {
this.rippleLines.shift()
this.ripplePoints.shift()
}
// 当达到条件时,添加数据
if (this.rate - this.lastripple >= this.minInterval) {
this.rippleLines.push({
r: this.radius + this.params.borderWidth + this.params.rippleWidth / 2,
color: utils.getRgbColor(this.params.rippleColor)
})
this.ripplePoints.push({
angle: utils.randomAngle()
})
// 更新添加时间
this.lastripple = this.rate
}
// 计算下一次渲染的位置数据
this.rippleLines = this.rippleLines.map((line, index) => ...)
this.ripplePoints = this.rippleLines.map((line, index) => ...)
// 根据新的数据渲染
this.strokeRippleLine()
this.strokeRipplePoint()
}
}
复制代码
每渲染一次即更新一次数据,将 requestAnimationFrame
存储于 this.frame
中,方便取消。
class Ripple{
animate() {
this.ctx.clearRect(0, 0, this.params.size, this.params.size)
this.strokeRipple()
this.strokeBorder()
...
var that = this
this.frame = requestAnimationFrame(function () {
that.animate()
})
}
}
复制代码
原理以下:
<audio>
,作好相关设置(自动播放、控件显示等等)可是,多是 audiocontext
的兼容问题,在 safari 中,没法实时获取到音频信息,若是有大神知晓,望不吝赐教。
因此这部分的实现并无什么好讲的了,有兴趣的能够直接查看 源码 及 实现
上述动画的实现的确并不复杂,可是在实现的过程当中,可能考虑到的更多的是如何组织代码,如何设计接口(怎么方便使用,增长定制度,减小操做度)。这些东西在写上面教程的时候轻猫淡写,一笔带过或者压根没提过,但只有在本身写时才能体会到切实的须要,因此,若是你能看到这里,不妨放下刚才看到的代码实现,只看效果,本身也来写一个(带音乐的)这样的动画,毕竟,咱们不缺乏发现简单的眼睛,缺乏的多是完美简单的手。
我以为挺好但好像并没人知道...