純動畫效果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 中,沒法實時獲取到音頻信息,若是有大神知曉,望不吝賜教。
因此這部分的實現並無什麼好講的了,有興趣的能夠直接查看 源碼 及 實現
上述動畫的實現的確並不複雜,可是在實現的過程當中,可能考慮到的更多的是如何組織代碼,如何設計接口(怎麼方便使用,增長定製度,減小操做度)。這些東西在寫上面教程的時候輕貓淡寫,一筆帶過或者壓根沒提過,但只有在本身寫時才能體會到切實的須要,因此,若是你能看到這裏,不妨放下剛纔看到的代碼實現,只看效果,本身也來寫一個(帶音樂的)這樣的動畫,畢竟,咱們不缺乏發現簡單的眼睛,缺乏的多是完美簡單的手。
我以爲挺好但好像並沒人知道...