項目中需要一個請求進度效果,嘗試了下自己用 canvas 來繪制一個環形進度條,動效直接用的休眠函數加隨機數來模擬。用到了 es6 里的 class 類,用單例模式的懶漢模式來實例化對象,不像 Java 這種純面向對象的語言,寫著還是有點別扭。
1.png
import NP from 'number-precision'
/**
* 休眠函數
* @param {Number} wait
*/
function sleep(wait) {
return new Promise((resolve) => {
setTimeout(resolve, wait)
})
}
export default class CanvasProgress {
constructor({ elementId, height = 200, width = 200 }) {
if (elementId) {
this.canvas = document.getElementById(elementId) // canvas 節點
this.canvas.height = height
this.canvas.width = width
this.elementId = elementId
this.height = height
this.width = width
this.cxt = canvas.getContext('2d') // 繪圖上下文
}
// this.instance = null
this.reset()
}
/**
* 設置進度
* @param {boolean} value
*/
setStep(value) {
this.step = value
}
/**
* 設置是否暫停
* @param {boolean} value
*/
setIsPause(value) {
this.isPause = value
}
/**
* 設置是否結束
* @param {boolean} value
*/
setIsEnd(value) {
this.isEnd = value
}
/**
* 重置
*/
reset() {
this.setStep(0)
this.setIsPause(false)
this.setIsEnd(false)
}
/**
* 獲取實例,單例模式
* @param {Object} config
* @returns {CanvasProgress} 實例對象
*/
static getInstance(config) {
const { elementId, height, width } = config
// 這里比較要用 instance 實例,不能直接用 this
const ins = this.instance
if (!ins || (ins && (elementId !== ins.elementId || height !== ins.height || width !== ins.width))) {
this.instance = new CanvasProgress(config)
}
return this.instance
}
/**
* 初始化
* @param {string} e 初始化類型:restart-重啟
*/
async init(e) {
const restart = e === 'restart'
let isStarted = false // 是否已經開啟了
let isPaused = false // 是否已經暫停了
if (!restart) {
isStarted = this.step > 0
isPaused = this.isPause || this.step === 100
this.reset()
}
this.start({ isStarted, isPaused })
}
/**
* 開啟
* @param {boolean} param.isStarted 是否已經開啟,若開啟了只用修改 step 數據,繼續使用開啟的 while 循環
* @param {boolean} param.isPaused 是否已經暫停,若暫停了需重新開啟 while 循環
*/
async start({ isStarted, isPaused } = {}) {
while( this.step < 100) {
if (this.isPause) return
if (isStarted) {
if (isPaused) this.start() // 暫停了的要重新開啟個循環
return
}
if (this.isEnd) {
await sleep(50)
this.step = parseInt(this.step)
if (this.step < 100) {
this.step++
}
this.draw()
continue
// return
}
// 生成 1-9之間的隨機數
const random = Math.round(Math.random() * 8) + 1
const num = NP.divide(random, Math.pow(10, random))
if (this.step < 80) {
await sleep(100)
this.step = NP.plus(this.step, (random > 5 ? random - 5 : random))
} else if (this.step >= 80 && this.step < 99.98) {
await sleep(10 * random)
this.step = NP.plus(this.step, num).toFixed(2)
} else {
// 接口還沒返回數據要處理下,否則無限死循環會內存溢出
// await sleep(1000)
// continue
// 直接 return 或暫停了,成功時再重啟
this.pause()
}
// 大于100時修正
if (this.step > 100) this.step = 100
this.draw()
}
}
/**
* 暫停
*/
pause() {
this.setIsPause(true)
}
/**
* 重啟
*/
restart() {
this.setIsPause(false)
this.init('restart')
}
/**
* 結束
*/
end() {
this.setIsEnd(true)
if (this.isPause) this.restart()
}
/**
* 繪圖
*/
draw() {
this.clearRect()
const x = this.width / 2
const y = this.height / 2
// 灰色背景
this.cxt.beginPath()
this. cxt.moveTo(x, y)
this.cxt.arc(x, y, x, 0, Math.PI * 2)
this.cxt.fillStyle='#ddd'
this.cxt.fill()
this.cxt.closePath()
// 進度
this.cxt.beginPath()
this.cxt.moveTo(100,100)
// arc(圓的中心x坐標, 圓的中心y坐標, 圓半徑, 起始角, 結束角[, 逆/順時針])
this.cxt.arc(x, y, x, -Math.PI * 0.5, Math.PI * 2 * this.step / 100 - Math.PI * 0.5, false)
this.cxt.fillStyle='#57bc78'
this.cxt.fill()
this.cxt.closePath()
// 頂層中間白色圓圈遮擋
this.cxt.beginPath()
this.cxt.moveTo(x, y)
this.cxt.arc(x, y, 80, 0, Math.PI * 2)
this.cxt.fillStyle="#fff"
this.cxt.fill()
this.cxt.closePath()
// 文字
this.cxt.textAlign = 'center'
this.cxt.fillStyle='#57bc78'
this.cxt.textBaseline = 'middle'
this.cxt.font = 'bold 24px Arial'
this.cxt.fillText(this.step + '%', x, y)
}
/**
* 清除繪圖區域
*/
clearRect() {
this.cxt.clearRect(0, 0, this.width, this.height)
}
/**
* 保存圖片
*/
saveImg() {
const url = this.canvas.toDataURL()
let a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', 'img.png')
a.setAttribute('target', '_blank')
document.body.appendChild(a)
a.dispatchEvent(new MouseEvent('click'))
document.body.removeChild(a)
}
}