用canvas畫一輪明月,夜空與流星

今天是中秋節,因而突發奇想,欸不如用canvas來畫一畫月亮吧。javascript

因而一副用canvas畫出的星空就這樣誕生了。java

Demo

在這裏我用了ES6語法,星星,月亮和流星都單獨寫成了一個module。git

因而我把js一共分紅這四個文件:main.js, Moon.js, Stars.js和Meteor.js,後面三個各自export出一個類。github

源碼

爲了方便,用了gulp作自動化的工具。gulp

main.js

import Stars    from './Stars'
import Moon     from './Moon'
import Meteor   from './Meteor'

let canvas = document.getElementById('canvas'),
    ctx = canvas.getContext('2d'),
    width = window.innerWidth,
    height = window.innerHeight,
    //實例化月亮和星星。流星是隨機時間生成,因此只初始化數組
    moon = new Moon(ctx, width, height),
    stars = new Stars(ctx, width, height, 200),
    meteors = [],
    count = 0

canvas.width = width
canvas.height = height

//流星生成函數
const meteorGenerator = ()=> {
    //x位置偏移,以避免通過月亮
    let x = Math.random() * width + 800
    meteors.push(new Meteor(ctx, x, height))

    //每隔隨機時間,生成新流星
    setTimeout(()=> {
        meteorGenerator()
    }, Math.random() * 2000)
}

//每一幀動畫生成函數
const frame = ()=> {
    //每隔10幀星星閃爍一次,節省計算資源
    count++
    count % 10 == 0 && stars.blink()

    moon.draw()
    stars.draw()

    meteors.forEach((meteor, index, arr)=> {
        //若是流星離開視野以內,銷燬流星實例,回收內存
        if (meteor.flow()) {
            meteor.draw()
        } else {
            arr.splice(index, 1)
        }
    })
    requestAnimationFrame(frame)
}

meteorGenerator()
frame()

開頭分別引入了另外三個module,分別是星星,月亮和流星。canvas

接着初始化了月亮和星星,但因爲流星是不定時隨機生成的,因此初始化一個數組用來保存接下來生成的流星。數組

在每一幀中,分別調用moon,star和meteor的draw函數,用來畫出每一幀,特別的,由於星星須要閃爍,流星須要移動,因此在draw以前對半徑和座標進行處理。若是流星跑出了canvas外,就從數組中清除相應的流星,從而解除引用和回收內存。dom

Moon.js

export default class Moon {
    constructor(ctx, width, height) {
        this.ctx = ctx
        this.width = width
        this.height = height
    }

    draw() {
        let ctx = this.ctx,
            gradient = ctx.createRadialGradient(
            200, 200, 80, 200, 200, 800)
        //徑向漸變
        gradient.addColorStop(0, 'rgb(255,255,255)')
        gradient.addColorStop(0.01, 'rgb(70,70,80)')
        gradient.addColorStop(0.2, 'rgb(40,40,50)')
        gradient.addColorStop(0.4, 'rgb(20,20,30)')
        gradient.addColorStop(1, 'rgb(0,0,10)')
        ctx.save()
        ctx.fillStyle = gradient
        ctx.fillRect(0, 0, this.width, this.height)
        ctx.restore()
    }
}

這是月亮的類,主要用到了canvas裏的徑向漸變效果。爲了達到和諧的程度,我試了很久T_T...函數

Stars.js

export default class Stars {
    constructor(ctx, width, height, amount) {
        this.ctx = ctx
        this.width = width
        this.height = height
        this.stars = this.getStars(amount)
    }

    getStars(amount) {
        let stars = []
        while (amount--) {
            stars.push({
                x: Math.random() * this.width,
                y: Math.random() * this.height,
                r: Math.random() + 0.2
            })
        }
        return stars
    }

    draw() {
        let ctx = this.ctx
        ctx.save()
        ctx.fillStyle = 'white'
        this.stars.forEach(star=> {
            ctx.beginPath()
            ctx.arc(star.x, star.y, star.r, 0, 2 * Math.PI)
            ctx.fill()
        })
        ctx.restore()
    }

    //閃爍,星星半徑每隔10幀隨機變大或變小
    blink() {
        this.stars = this.stars.map(star=> {
            let sign = Math.random() > 0.5 ? 1 : -1
            star.r += sign * 0.2
            if (star.r < 0) {
                star.r = -star.r
            } else if (star.r > 1) {
                star.r -= 0.2
            }
            return star
        })

    }
}

星星的集合。由於不至於給每個星星都寫成單獨的對象,因而就寫了一個星星的集合類,全部的星星都保存在實例的stars中。其中的blink函數用來隨機改變每個星星的半徑大小,從而產生閃爍的效果。工具

Meteor.js

export default class Meteor {
    constructor(ctx, x, h) {
        this.ctx = ctx
        this.x = x
        this.y = 0
        this.h = h
        this.vx = -(4 + Math.random() * 4)
        this.vy = -this.vx
        this.len = Math.random() * 300 + 500
    }

    flow() {
        //斷定流星出界
        if (this.x < -this.len || this.y > this.h + this.len) {
            return false
        }
        this.x += this.vx
        this.y += this.vy
        return true
    }

    draw() {
        let ctx = this.ctx,
            //徑向漸變,從流星頭尾圓心,半徑越大,透明度越高
            gra = ctx.createRadialGradient(
                this.x, this.y, 0, this.x, this.y, this.len)

        const PI = Math.PI
        gra.addColorStop(0, 'rgba(255,255,255,1)')
        gra.addColorStop(1, 'rgba(0,0,0,0)')
        ctx.save()
        ctx.fillStyle = gra
        ctx.beginPath()
        //流星頭,二分之一圓
        ctx.arc(this.x, this.y, 1, PI / 4, 5 * PI / 4)
        //繪製流星尾,三角形
        ctx.lineTo(this.x + this.len, this.y - this.len)
        ctx.closePath()
        ctx.fill()
        ctx.restore()
    }
}

流星就比較有意思啦。猜猜每個流星是怎麼畫的?

實際上每個流星的輪廓由一個半圓和一個三角形組成,相似於一個不倒翁。而後總體傾角45度,而且填充時用上一個徑向漸變,就能夠至關完美的達到流行尾巴那樣漸行漸遠漸模糊的樣子。

對,就是這麼幹淨利落~

最後看了一下CPU和GPU的佔用,還好,優化的還比較到位,我那渣族手機都能跑的很流暢...

今天是中秋節,惋惜我這下雨了...沒月亮可看...

不過我有了這個月亮。

「希望人長久,千里共嬋娟」,千里以外的朋友,看到同一輪「明月」,也是緣分吧~

相關文章
相關標籤/搜索