用canvas 畫煙花

開始

最終效果: codepencanvas

一開始都是一個單一的用例,定位在畫布中央,再擴展開來數組

先獲取canvas元素及可視寬高dom

let canvas = document.querySelector('#canvas')
    let context = canvas.getContext('2d')
    let cw = canvas.width = window.innerWidth
    let ch = canvas.height = window.innerHeight
複製代碼

開始繪製

第一部分-定位的用的閃爍的圓

// 建立一個閃爍圓的類
class Kirakira {
    constructor(){
        // 目標點,這裏先指定爲屏幕中央
        this.targetLocation = {x: cw/2, y: ch/2}
        this.radius = 1
    }
    draw() {
        // 繪製一個圓
        context.beginPath()
        context.arc(this.targetLocation.x, this.targetLocation.y, 5, 0, Math.PI * 2)
        context.lineWidth = 2
        context.strokeStyle = '#FFFFFF';
        context.stroke()
    }

    update(){
        if(this.radius < 5){
            this.radius += 0.3
        }else{
            this.radius = 1
        }
    }

    init() {
        this.draw()
    }
}

class Animate {
    run() {
        window.requestAnimationFrame(this.run.bind(this))
        if(o){
            o.init()
        }
    }
}

let o = new Kirakira()
let a = new Animate()
a.run()
複製代碼

由此,能夠看到一個由小到大擴張的圓。因爲沒有擦除上一幀,每一幀的繪製結果都顯示出來,因此呈現出來的是一個實心的圓。我想繪製的是一個閃爍的圓,那麼能夠把上一幀給擦除。函數

context.clearRect(0, 0, cw, ch)
複製代碼

第二部分-畫射線

首先,先畫一由底部到畫布中央的延伸線。既然是運動的延伸線條,那起碼會有一個起點座標和一個終點座標動畫

class Biubiubiu {
    constructor(startX, startY, targetX, targetY){
        this.startLocation = {x: startX, y: startY}
        // 運動當前的座標,初始默認爲起點座標
        this.nowLoaction = {x: startX, y: startY}
        this.targetLocation = {x: targetX, y: targetY}
    }
    draw(){
        context.beginPath()
        context.moveTo(this.startLocation.x, this.startLocation.y)
        context.lineWidth = 3
        context.lineCap = 'round'
        // 線條須要定位到當前的運動座標,才能使線條運動起來
        context.lineTo(this.nowLoaction.x, this.nowLoaction.y)
        context.strokeStyle = '#FFFFFF'
        context.stroke()   
    }
    update(){}
    init(){
        this.draw()
        this.update()
    }
}
class Animate {
    run() {
        window.requestAnimationFrame(this.run.bind(this))
        context.clearRect(0, 0, cw, ch)
        if(b){
            b.init()
        }
    }
}
// 這裏的打算是定位起點在畫布的底部隨機位置, 終點在畫布中央。
let b = new Biubiubiu(Math.random()*(cw/2), ch, cw/2, ch/2)
let a = new Animate()
a.run()
複製代碼

說說三角函數

已知座標起點和座標終點, 那麼問題來了,要怎麼知道從起點到終點的每一幀的座標呢 ui

如圖。大概須要作判斷的目標有

  1. 線條運動的距離是否超出起點到終點的距離,如超出則須要中止運動
  2. 每一幀運動到達的座標

計算距離

對於座標間距離的計算,很明顯的可使用勾股定理完成。
設起點座標爲x0, y0, 終點座標爲x1, y1 ,便可得 distance = √(x1-x0)² + (y1-y0)²,用代碼表示則是Math.sqrt(Math.pow((x1-x0), 2) + Math.pow((y1-y0), 2))this

計算座標

上一幀的總距離(d) + 當前幀下走過的路程(v) = 當前幀的距離(D)
假設一個速度 speed = 2, 起點和終點造成的角度爲(θ), 路程(v)的座標分別爲vx, vy
那麼 vx = cos(θ) * speed, vy = sin(θ) * speed 因爲起點(x0, y0)和終點(x1, y1)已知,由圖可知,經過三角函數中的tan能夠取到兩點成線和水平線之間的夾角角度,代碼表示爲Math.atan2(y1 - y0, x1 - x0)spa

回到繪製延伸線的代碼。 給Biubiubiu類添加上角度和距離的計算,code

class Biubiubiu {
    constructor(startX, startY, targetX, targetY){
        ...
        // 到目標點的距離
        this.targetDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.targetLocation.x, this.targetLocation.y);
        // 速度
        this.speed = 2
        // 角度
        this.angle = Math.atan2(this.targetLocation.y - this.startLocation.y, this.targetLocation.x - this.startLocation.x)
        // 是否到達目標點
        this.arrived = false
    }
    
    draw(){ ... }
    
    update(){
        // 計算當前幀的路程v
        let vx = Math.cos(this.angle) * this.speed
        let vy = Math.sin(this.angle) * this.speed
        // 計算當前運動距離
        let nowDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.nowLoaction.x+vx, this.nowLoaction.y+vy)
        // 若是當前運動的距離超出目標點距離,則不須要繼續運動
        if(nowDistance >= this.targetDistance){
            this.arrived = true
        }else{
            this.nowLoaction.x += vx
            this.nowLoaction.y += vy
            this.arrived = false
        }
    }
    
    getDistance(x0, y0, x1, y1) {
        // 計算兩座標點之間的距離
        let locX = x1 - x0
        let locY = y1 - y0
        // 勾股定理
        return Math.sqrt(Math.pow(locX, 2) + Math.pow(locY, 2))
    }
    
    init(){
        this.draw()
        this.update()
    }
}
class Animate { ... }
// 這裏的打算是定位起點在畫布的底部隨機位置, 終點在畫布中央。
let b = new Biubiubiu(Math.random()*(cw/2), ch, cw/2, ch/2)
let a = new Animate()
a.run()
複製代碼

因爲speed是固定的,這裏呈現的是勻速運動。能夠加個加速度``,使其改變爲變速運動。 個人目標效果並非一整條線條,而是當前運行的一截線段軌跡。這裏有個思路,把必定量的座標點存爲一個數組,在繪製的時候能夠由數組內的座標指向當前運動的座標,並在隨着幀數變化不停對數組進行數據更替,由此能夠繪製出一小截的運動線段cdn

實現代碼:

class Biubiubiu {
    constructor(startX, startY, targetX, targetY) {
        ...
        // 線段集合, 每次存10個,取10個幀的距離
        this.collection = new Array(10)
    }
    draw() {
        context.beginPath()
        // 這裏改成由集合的第一位開始定位
        try{
            context.moveTo(this.collection[0][0], this.collection[0][1])
        }catch(e){
            context.moveTo(this.nowLoaction.x, this.nowLoaction.y)
        }
        ...
    }
    
    update(){
        // 對集合進行數據更替,彈出數組第一個數據,並把當前運動的座標push到集合。只要取數組的頭尾兩個座標相連,則是10個幀的長度
        this.collection.shift()
        this.collection.push([this.nowLoaction.x, this.nowLoaction.y])
        // 給speed添加加速度
        this.speed *= this.acceleration
        ...
    }
}
複製代碼

第三部分-畫一個爆炸的效果

由上面的延伸線的代碼,擴展開來,若是不取10幀,取個兩三幀的小線段,而後改變延伸方向,多條射線組合,就能夠造成了爆炸效果。火花是會受重力,摩擦力等影響到,擴散趨勢是偏向下的,因此須要加上一些重力,摩擦力系數

class Boom {
    // 爆炸物是沒有肯定的結束點座標, 這個能夠經過設定必定的閥值來限定
    constructor(startX, startY){
        this.startLocation = {x: startX, y: startY}
        this.nowLocation = {x: startX, y: startY}
        // 速度
        this.speed = Math.random()*10+2
        // 加速度
        this.acceleration = 0.95
        // 沒有肯定的結束點,因此沒有固定的角度,能夠隨機角度擴散
        this.angle = Math.random()*Math.PI*2
        // 這裏設置閥值爲100
        this.targetCount = 100
        // 當前計算爲1,用於判斷是否會超出閥值
        this.nowNum = 1
        // 透明度
        this.alpha = 1
        // 重力系數
        this.gravity = 0.98
        this.decay = 0.015
        
        // 線段集合, 每次存10個,取10個幀的距離
        this.collection = new Array(CONFIG.boomCollectionCont)
        
        // 是否到達目標點
        this.arrived = false
    }

    draw(){
        context.beginPath()
        try{
            context.moveTo(this.collection[0][0], this.collection[0][1])
        }catch(e){
            context.moveTo(this.nowLocation.x, this.nowLocation.y)
        }
        context.lineWidth = 3
        context.lineCap = 'round'
        context.lineTo(this.nowLocation.x, this.nowLocation.y)
        // 設置由透明度減少產生的漸隱效果,看起來沒這麼突兀
        context.strokeStyle = `rgba(255, 255, 255, ${this.alpha})`
        context.stroke()
    }

    update(){
        this.collection.shift()
        this.collection.push([this.nowLocation.x, this.nowLocation.y])
        this.speed *= this.acceleration
        
        let vx = Math.cos(this.angle) * this.speed
        // 加上重力系數,運動軌跡會趨向下
        let vy = Math.sin(this.angle) * this.speed + this.gravity

        // 當前計算大於閥值的時候的時候,開始進行漸隱處理
        if(this.nowNum >= this.targetCount){
            this.alpha -= this.decay
        }else{
            this.nowLocation.x += vx
            this.nowLocation.y += vy
            this.nowNum++
        }

        // 透明度爲0的話,能夠進行移除處理,釋放空間
        if(this.alpha <= 0){
            this.arrived = true
        }
    }

    init(){
        this.draw()
        this.update()
    }
}

class Animate {
    constructor(){
        // 定義一個數組作爲爆炸點的集合
        this.booms = []
        // 避免每幀都進行繪製致使的過量繪製,設置閥值,到達閥值的時候再進行繪製
        this.timerTarget = 80
        this.timerNum = 0
    }
    
    pushBoom(){
        // 實例化爆炸效果,隨機條數的射線擴散
        for(let bi = Math.random()*10+20; bi>0; bi--){
            this.booms.push(new Boom(cw/2, ch/2))
        }
    }

    run() {
        window.requestAnimationFrame(this.run.bind(this))
        context.clearRect(0, 0, cw, ch)
        
        let bnum = this.booms.length
        while(bnum--){
            // 觸發動畫
            this.booms[bnum].init()
            if(this.booms[bnum].arrived){
                // 到達目標透明度後,把炸點給移除,釋放空間
                this.booms.splice(bnum, 1)
            }
        }
        
        if(this.timerNum >= this.timerTarget){
            // 到達閥值,進行爆炸效果的實例化
            this.pushBoom()
            this.timerNum = 0
        }else{
            this.timerNum ++ 
        }
    }
}

let a = new Animate()
a.run()
複製代碼

第四部分-合併代碼,而且由一到多

合併代碼的話,主要是個順序問題。
地點上,閃爍圓的座標點便是射線的目標終點,同時也是爆炸效果的座標起點。 時間上,在和射線到達終點後,再觸發爆炸方法便可。

let canvas = document.querySelector('#canvas')
let context = canvas.getContext('2d')
let cw = canvas.width = window.innerWidth
let ch = canvas.height = window.innerHeight

function randomColor(){
    // 返回一個0-255的數值,三個隨機組合爲一塊兒可定位一種rgb顏色
    let num = 3
    let color = []
    while(num--){
        color.push(Math.floor(Math.random()*254+1))
    }
    return color.join(', ')
}

class Kirakira {
    constructor(targetX, targetY){
        // 指定產生的座標點
        this.targetLocation = {x: targetX, y: targetY}
        this.radius = 1
    }
    draw() {
        // 繪製一個圓
        context.beginPath()
        context.arc(this.targetLocation.x, this.targetLocation.y, this.radius, 0, Math.PI * 2)
        context.lineWidth = 2
        context.strokeStyle = `rgba(${randomColor()}, 1)`;
        context.stroke()
    }

    update(){
        // 讓圓進行擴張,實現閃爍效果
        if(this.radius < 5){
            this.radius += 0.3
        }else{
            this.radius = 1
        }
    }

    init() {
        this.draw()
        this.update()
    }
}

class Biubiubiu {
    constructor(startX, startY, targetX, targetY) {
        this.startLocation = {x: startX, y: startY}
        this.targetLocation = {x: targetX, y: targetY}
        // 運動當前的座標,初始默認爲起點座標
        this.nowLoaction = {x: startX, y: startY}
        // 到目標點的距離
        this.targetDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.targetLocation.x, this.targetLocation.y);
        // 速度
        this.speed = 2
        // 加速度
        this.acceleration = 1.02
        // 角度
        this.angle = Math.atan2(this.targetLocation.y - this.startLocation.y, this.targetLocation.x - this.startLocation.x)
        
        // 線段集合
        this.collection = []
        // 線段集合, 每次存10個,取10個幀的距離
        this.collection = new Array(CONFIG.biuCollectionCont)
        // 是否到達目標點
        this.arrived = false
    }

    draw() {
        context.beginPath()
        try{
            context.moveTo(this.collection[0][0], this.collection[0][1])
        }catch(e){
            context.moveTo(this.nowLoaction.x, this.nowLoaction.y)
        }
        context.lineWidth = 3
        context.lineCap = 'round'
        context.lineTo(this.nowLoaction.x, this.nowLoaction.y)
        context.strokeStyle = `rgba(${randomColor()}, 1)`;
        context.stroke()                                
    }

    update() {
        this.collection.shift()
        this.collection.push([this.nowLoaction.x, this.nowLoaction.y])
        this.speed *= this.acceleration
        let vx = Math.cos(this.angle) * this.speed
        let vy = Math.sin(this.angle) * this.speed
        let nowDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.nowLoaction.x+vx, this.nowLoaction.y+vy)
        if(nowDistance >= this.targetDistance){
            this.arrived = true
        }else{
            this.nowLoaction.x += vx
            this.nowLoaction.y += vy
            this.arrived = false
        }
    }

    getDistance(x0, y0, x1, y1) {
        // 計算兩座標點之間的距離
        let locX = x1 - x0
        let locY = y1 - y0
        // 勾股定理
        return Math.sqrt(Math.pow(locX, 2) + Math.pow(locY, 2))
    }

    init() {
        this.draw()
        this.update()
    }
}

class Boom {
    // 爆炸物是沒有肯定的結束點座標, 這個能夠經過設定必定的閥值來限定
    constructor(startX, startY){
        this.startLocation = {x: startX, y: startY}
        this.nowLocation = {x: startX, y: startY}
        // 速度
        this.speed = Math.random()*10+2
        // 加速度
        this.acceleration = 0.95
        // 沒有肯定的結束點,因此沒有固定的角度,能夠隨機角度擴散
        this.angle = Math.random()*Math.PI*2
        // 這裏設置閥值爲100
        this.targetCount = 100
        // 當前計算爲1,用於判斷是否會超出閥值
        this.nowNum = 1
        // 透明度
        this.alpha = 1
        // 透明度減小梯度
        this.grads = 0.015
        // 重力系數
        this.gravity = 0.98
        
        // 線段集合, 每次存10個,取10個幀的距離
        this.collection = new Array(10)
        
        // 是否到達目標點
        this.arrived = false
    }

    draw(){
        context.beginPath()
        try{
            context.moveTo(this.collection[0][0], this.collection[0][1])
        }catch(e){
            context.moveTo(this.nowLoaction.x, this.nowLoaction.y)
        }
        context.lineWidth = 3
        context.lineCap = 'round'
        context.lineTo(this.nowLocation.x, this.nowLocation.y)
        // 設置由透明度減少產生的漸隱效果,看起來沒這麼突兀
        context.strokeStyle = `rgba(${randomColor()}, ${this.alpha})`
        context.stroke()
    }

    update(){
        this.collection.shift()
        this.collection.push([this.nowLocation.x, this.nowLocation.y])
        this.speed *= this.acceleration
        
        let vx = Math.cos(this.angle) * this.speed
        // 加上重力系數,運動軌跡會趨向下
        let vy = Math.sin(this.angle) * this.speed + this.gravity

        // 當前計算大於閥值的時候的時候,開始進行漸隱處理
        if(this.nowNum >= this.targetCount){
            this.alpha -= this.grads
        }else{
            this.nowLocation.x += vx
            this.nowLocation.y += vy
            this.nowNum++
        }

        // 透明度爲0的話,能夠進行移除處理,釋放空間
        if(this.alpha <= 0){
            this.arrived = true
        }
    }

    init(){
        this.draw()
        this.update()
    }
}

class Animate {
    constructor(){
        // 用於記錄當前實例化的座標點
        this.startX = null
        this.startY = null
        this.targetX = null
        this.targetY = null
        // 定義一個數組作爲閃爍球的集合
        this.kiras = []
        // 定義一個數組作爲射線類的集合
        this.bius = []
        // 定義一個數組作爲爆炸類的集合
        this.booms = []
        // 避免每幀都進行繪製致使的過量繪製,設置閥值,到達閥值的時候再進行繪製
        this.timerTarget = 80
        this.timerNum = 0
    }

    pushBoom(x, y){
        // 實例化爆炸效果,隨機條數的射線擴散
        for(let bi = Math.random()*10+20; bi>0; bi--){
            this.booms.push(new Boom(x, y))
        }
    }

    run() {
        window.requestAnimationFrame(this.run.bind(this))
        context.clearRect(0, 0, cw, ch)
        
        let biuNum = this.bius.length
        while(biuNum-- ){
            this.bius[biuNum].init()
            this.kiras[biuNum].init()
            if(this.bius[biuNum].arrived){
                // 到達目標後,能夠開始繪製爆炸效果, 當前線條的目標點則是爆炸實例的起始點
                this.pushBoom(this.bius[biuNum].nowLoaction.x, this.bius[biuNum].nowLoaction.y)

                // 到達目標後,把當前類給移除,釋放空間
                this.bius.splice(biuNum, 1)
                this.kiras.splice(biuNum, 1)
            }
        }

        let bnum = this.booms.length
        while(bnum--){
            // 觸發動畫
            this.booms[bnum].init()
            if(this.booms[bnum].arrived){
                // 到達目標透明度後,把炸點給移除,釋放空間
                this.booms.splice(bnum, 1)
            }
        }

        if(this.timerNum >= this.timerTarget){
            // 到達閥值後開始繪製實例化射線
            this.startX = Math.random()*(cw/2)
            this.startY = ch
            this.targetX = Math.random()*cw
            this.targetY = Math.random()*(ch/2)
            let exBiu = new Biubiubiu(this.startX, this.startY, this.targetX, this.targetY)
            let exKira = new Kirakira(this.targetX, this.targetY)
            this.bius.push(exBiu)
            this.kiras.push(exKira)
            // 到達閥值後把當前計數重置一下
            this.timerNum = 0
        }else{
            this.timerNum ++ 
        }
    }
}

let a = new Animate()
a.run()
複製代碼

製做過程當中衍生出來的比較好玩的效果

  1. codepen
  2. codepen
相關文章
相關標籤/搜索