用canvas繪製流星夜空

coldday發表於2019-04-20

流星是一種唯美的天文現象,我一度想用所學知識將它繪製,最近閱讀MDN上的canvas教程得到啟發,用一個canvas的長尾效果繪製流星……

用canvas繪製流星夜空

什麼是長尾效果?

我們知道,canvas動畫實現依賴於畫布的重繪,通過不停的清空畫布,繪製畫布就能實現基本的動畫效果。一般使用clearRect方法清除指定矩形區域,來實現重繪。長尾效果是使用透明的填充色代替clearRect方法來實現的。

使用clearRect

吐槽:由於錄屏軟體fps跟不上canvas所以這個gif圖有點卡頓

用canvas繪製流星夜空

使用fillRect

1.透明度為1

用canvas繪製流星夜空
可以看出透明度為1時,效果與清除效果一致,我們可以理解為在畫布上又蓋上了一畫布 ,遮住了之前所畫的內容所以和清除效果是一樣的。
2.透明度為0
用canvas繪製流星夜空
透明度為0的效果,則等價於沒有清除畫布的效果,此時運動的球體像一隻尾巴不斷變長的蛇,可以以此猜想,將透明度設為0~1之間,就能調整尾巴的長度,達到一個帶尾巴的運動模糊效果。
3.長尾效果
用canvas繪製流星夜空
因為不斷蓋上透明的畫布,所以從繪製點到出發點就產生了一條顏色不斷變淺的路徑,給人視覺效果就是一個動態模糊。

流星

可以將流星解構為一個圓形和長尾效果:

肉眼效果:

用canvas繪製流星夜空
實際效果:
用canvas繪製流星夜空

封裝頁面形狀

使用物件導向程式設計完成canvas繪製
月亮類

class Moon {
    constructor(x, y, ctx, r = 25) {
        this.x = x;
        this.y = y;
        this.ctx = ctx;
        this.r = r;
    }
    draw() {
        this.ctx.fillStyle = 'rgba(255,255,255,0.6)';
        this.ctx.shadowBlur = this.r + 5; //光暈半徑
        this.ctx.shadowColor = "#fff"; // 光暈顏色
        this.ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
        this.ctx.fill();
    }
}
複製程式碼

因為月亮是靜止在頁面上的,所以只有一個draw方法,月亮光暈的實現是把陰影和填充設為同一顏色,然後讓陰影透明度大於填充透明度,就形成一個外發光的效果。
星星類

class Star extends Moon {
    constructor(x, y, ctx, r) {
        super(x, y, ctx, r);
    }
    draw() {
        this.ctx.fillStyle = 'rgba(255,255,255,0.8)';
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
        this.ctx.closePath();
        this.ctx.fill();
    }
    move() {
        this.x += 0.08;
        if (this.x > meteorCanvas.width) {
            this.x = 0;
        }
        this.draw();
    }
}
複製程式碼

星星與月亮的唯一區別是可以移動,所以用星星類去繼承月亮類,實現物件導向的繼承與多型。
用星星緩慢的向右移動可以模擬地球自轉帶來的效果。
流星類

class Meteor extends Star {
    constructor(x, y, ctx, r,angle) {
        super(x, y, ctx, r);
        this.angle = angle;
    }
    draw() {
        this.ctx.fillStyle = '#ffffff';
        this.ctx.rotate(this.angle);
        this.ctx.translate(100, -meteorCanvas.height / 1.5);
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
        this.ctx.closePath();
        this.ctx.fill();
        this.ctx.translate(-100, meteorCanvas.height / 1.5);
        this.ctx.rotate(-this.angle);
    }
    move() {
        this.x += 4;
        this.y += 1;
        if (this.x > meteorCanvas.width) {
            this.x = Math.random() * 5
            this.y = -2 * meteorCanvas.height + Math.random() * meteorCanvas.height * 3;
        }
        this.draw();
    }
}
複製程式碼

同理用流星類去繼承星星類。
注意的是,流星類與星星類運動的不同之處是流星劃過天空有一個夾角,所以在繪製時將畫布旋轉了角度之後,需要回歸原位。
為了讓流星出現的位置不會太密集,我將流星在y軸出現的位置設定在-2倍畫布高度到1倍畫布高度之間,並在draw方法中將畫布往上挪了畫布高度的2/3(同理要將畫布歸位)。

繪製canvas

流星需要使用長尾效果渲染,星星需要clearRect重繪,月亮就只需要繪製一次。為了三種形狀互不干擾,我分別使用了不同畫布去渲染它們。
優點:互不干擾,繪製邏輯清晰,優化渲染。

原始碼

const meteorCanvas = document.getElementById('meteor');
        const starCanvas = document.getElementById('star');
        const moonCanvas = document.getElementById('moon');
        const meteors = [], stars = [];

        meteorCanvas.width = document.body.clientWidth;
        meteorCanvas.height = document.body.clientHeight;
        starCanvas.width = document.body.clientWidth;
        starCanvas.height = document.body.clientHeight / 3;
        moonCanvas.width = document.body.clientWidth;
        moonCanvas.height = document.body.clientHeight / 3;
        const meteorCtx = meteorCanvas.getContext('2d');
        const starCtx = starCanvas.getContext('2d');
        const moonCtx = moonCanvas.getContext('2d');

        init();
        animate();

        function init() {
            for (var i = 0; i < 4; i++) {
                meteors[i] = new Meteor(Math.random() * meteorCanvas.width,
                    -2 * meteorCanvas.height + Math.random() * meteorCanvas.height * 3,
                    meteorCtx, Math.floor(Math.random() * 2) + 1.5, Math.PI / 7);
                meteors[i].draw();
            }
            for (var i = 0; i < 60; i++) {
                stars[i] = new Star(Math.random() * starCanvas.width, Math.random() * starCanvas.height,
                    starCtx, Math.random());
                stars[i].draw();
            }
            moon = new Moon(moonCanvas.width - 50, 50, moonCtx)
            moon.draw();
        }
        function animate() {
            starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
            meteorCtx.fillStyle = `rgba(0, 0, 0, 0.1)`;
            meteorCtx.fillRect(0, 0, meteorCanvas.width, meteorCanvas.height);
            for (let meteor of meteors)
                meteor.move();
            for (let star of stars)
                star.move();
            requestAnimationFrame(animate);
        }
        function recover() {
            for (let meteor of meteors)
                meteor = null;
            for (let star of stars)
                star = null;
            moon = null;
        }
        window.onresize = function () {
            meteorCanvas.width = document.body.clientWidth;
            meteorCanvas.height = document.body.clientHeight;
            starCanvas.width = document.body.clientWidth;
            starCanvas.height = document.body.clientHeight / 3;
            moonCanvas.width = document.body.clientWidth;
            moonCanvas.height = document.body.clientHeight / 3;
            recover();
            init();
        }
複製程式碼

結語

陪你去看流星雨落在這地球上
讓你的淚落在我肩膀
要你相信我的愛只肯為你勇敢……

文章隨著《流星雨》的歌聲,也走向了尾聲。人生如流星劃過,轉瞬即逝,然而,流星易逝,真情永恆……

相關文章