撩妹技能 get,教你用 canvas 畫一場流星雨


開始

妹子都喜歡流星,如果她說不喜歡,那她一定是一個假妹子。

現在就一起來做一場流星雨,用程序員的野路子浪漫一下。

要畫一場流星雨,首先,自然我們要會畫一顆流星。

玩過 canvas 的同學,你畫圓畫方畫線條這么 6,如果說叫你畫下面這個玩意兒,你會不會覺得你用的是假 canvas?canvas 沒有畫一個帶尾巴玩意兒的 api 啊。

畫一顆流星

是的,的卻是沒這個 api,但是不代表我們畫不出來。流星就是一個小石頭,然后因為速度過快產生大量的熱量帶動周圍的空氣發光發熱,所以經飛過的地方看起來就像是流星的尾巴,我們先研究一下流星這個圖像,整個流星處於他自己的運動軌跡之中,當前的位置最亮,輪廓最清晰,而之前划過的地方離當前位置軌跡距離越遠就越暗淡越模糊。

上面的分析結果很關鍵, canvas 上是每一幀就重繪一次,每一幀之間的時間間隔很短。流星經過的地方會越來越模糊最后消失不見,那有沒有可以讓畫布畫的圖像每過一幀就變模糊一點而不是全部清除的辦法?如果可以這樣,就可以把每一幀用線段畫一小段流星的運動軌跡,最后畫出流星的效果。

騙紙!你也許會說,這那里像流星了???
別急,讓我多畫幾段給你看看。

什么? 還是不像? 我們把它畫小點,這下總該像了把?

 

上面幾幅圖我是在 ps 上模擬的,本質上 ps 也是在畫布上繪畫,我們馬上在 canvas 上試試。

那,直接代碼實現一下。

// 坐標
class Crood {
    constructor(x=0, y=0) {
        this.x = x;
        this.y = y;
    }
    setCrood(x, y) {
        this.x = x;
        this.y = y;
    }
    copy() {
        return new Crood(this.x, this.y);
    }
}

// 流星
class ShootingStar {
    constructor(init=new Crood, final=new Crood, size=3, speed=200, onDistory=null) {
        this.init = init; // 初始位置
        this.final = final; // 最終位置
        this.size = size; // 大小
        this.speed = speed; // 速度:像素/s

        // 飛行總時間
        this.dur = Math.sqrt(Math.pow(this.final.x-this.init.x, 2) + Math.pow(this.final.y-this.init.y, 2)) * 1000 / this.speed; 

        this.pass = 0; // 已過去的時間
        this.prev = this.init.copy(); // 上一幀位置
        this.now = this.init.copy(); // 當前位置
        this.onDistory = onDistory;
    }
    draw(ctx, delta) {
        this.pass += delta;
        this.pass = Math.min(this.pass, this.dur);

        let percent = this.pass / this.dur;

        this.now.setCrood(
            this.init.x + (this.final.x - this.init.x) * percent,
            this.init.y + (this.final.y - this.init.y) * percent
        );

        // canvas
        ctx.strokeStyle = '#fff';
        ctx.lineCap = 'round';
        ctx.lineWidth = this.size;
        ctx.beginPath();
        ctx.moveTo(this.now.x, this.now.y);
        ctx.lineTo(this.prev.x, this.prev.y);
        ctx.stroke();

        this.prev.setCrood(this.now.x, this.now.y);
        if (this.pass === this.dur) {
            this.distory();
        }
    }
    distory() {
        this.onDistory && this.onDistory();
    }
}


// effet
let cvs = document.querySelector('canvas');
let ctx = cvs.getContext('2d');

let T;
let shootingStar = new ShootingStar(
                        new Crood(100, 100), 
                        new Crood(400, 400),
                        3,
                        200,
                        ()=>{cancelAnimationFrame(T)}
                    );

let tick = (function() {
    let now = (new Date()).getTime();
    let last = now;
    let delta;
    return function() {
        delta = now - last;
        delta = delta > 500 ? 30 : (delta < 16? 16 : delta);
        last = now;
        // console.log(delta);

        T = requestAnimationFrame(tick);

        ctx.save();
        ctx.fillStyle = 'rgba(0,0,0,0.2)'; // 每一幀用 “半透明” 的背景色清除畫布
        ctx.fillRect(0, 0, cvs.width, cvs.height);
        ctx.restore();
        shootingStar.draw(ctx, delta);
    }
})();
tick();

效果:一顆流星

sogoyi 快看,一顆活潑不做作的流星!!! 是不是感覺動起來更加逼真一點?

流星雨

我們再加一個流星雨 MeteorShower 類,生成多一些隨機位置的流星,做出流星雨。

// 坐標
class Crood {
    constructor(x=0, y=0) {
        this.x = x;
        this.y = y;
    }
    setCrood(x, y) {
        this.x = x;
        this.y = y;
    }
    copy() {
        return new Crood(this.x, this.y);
    }
}

// 流星
class ShootingStar {
    constructor(init=new Crood, final=new Crood, size=3, speed=200, onDistory=null) {
        this.init = init; // 初始位置
        this.final = final; // 最終位置
        this.size = size; // 大小
        this.speed = speed; // 速度:像素/s

        // 飛行總時間
        this.dur = Math.sqrt(Math.pow(this.final.x-this.init.x, 2) + Math.pow(this.final.y-this.init.y, 2)) * 1000 / this.speed; 

        this.pass = 0; // 已過去的時間
        this.prev = this.init.copy(); // 上一幀位置
        this.now = this.init.copy(); // 當前位置
        this.onDistory = onDistory;
    }
    draw(ctx, delta) {
        this.pass += delta;
        this.pass = Math.min(this.pass, this.dur);

        let percent = this.pass / this.dur;

        this.now.setCrood(
            this.init.x + (this.final.x - this.init.x) * percent,
            this.init.y + (this.final.y - this.init.y) * percent
        );

        // canvas
        ctx.strokeStyle = '#fff';
        ctx.lineCap = 'round';
        ctx.lineWidth = this.size;
        ctx.beginPath();
        ctx.moveTo(this.now.x, this.now.y);
        ctx.lineTo(this.prev.x, this.prev.y);
        ctx.stroke();

        this.prev.setCrood(this.now.x, this.now.y);
        if (this.pass === this.dur) {
            this.distory();
        }
    }
    distory() {
        this.onDistory && this.onDistory();
    }
}

class MeteorShower {
    constructor(cvs, ctx) {
        this.cvs = cvs;
        this.ctx = ctx;
        this.stars = [];
        this.T;
        this.stop = false;
        this.playing = false;
    }

    createStar() {
        let angle = Math.PI / 3;
        let distance = Math.random() * 400;
        let init = new Crood(Math.random() * this.cvs.width|0, Math.random() * 100|0);
        let final = new Crood(init.x + distance * Math.cos(angle), init.y + distance * Math.sin(angle));
        let size = Math.random() * 2;
        let speed = Math.random() * 400 + 100;
        let star = new ShootingStar(
                        init, final, size, speed, 
                        ()=>{this.remove(star)}
                    );
        return star;
    }

    remove(star) {
        this.stars = this.stars.filter((s)=>{ return s !== star});
    }

    update(delta) {
        if (!this.stop && this.stars.length < 20) {
            this.stars.push(this.createStar());
        }
        this.stars.forEach((star)=>{
            star.draw(this.ctx, delta);
        });
    }

    tick() {
        if (this.playing) return;
        this.playing = true;

        let now = (new Date()).getTime();
        let last = now;
        let delta;

        let  _tick = ()=>{
            if (this.stop && this.stars.length === 0) {
                cancelAnimationFrame(this.T);
                this.playing = false;
                return;
            }

            delta = now - last;
            delta = delta > 500 ? 30 : (delta < 16? 16 : delta);
            last = now;
            // console.log(delta);

            this.T = requestAnimationFrame(_tick);

            ctx.save();
            ctx.fillStyle = 'rgba(0,0,0,0.2)'; // 每一幀用 “半透明” 的背景色清除畫布
            ctx.fillRect(0, 0, cvs.width, cvs.height);
            ctx.restore();
            this.update(delta);
        }
        _tick();
    }

    start() {
        this.stop = false;
        this.tick();
    }

    stop() {
        this.stop = true;
    }  
}

// effet
let cvs = document.querySelector('canvas');
let ctx = cvs.getContext('2d');

let meteorShower = new MeteorShower(cvs, ctx);
meteorShower.start();

效果:流星雨

透明背景

先不急着激動,這個流星雨有點單調,可以看到上面的代碼中,每一幀,我們用了透明度為 0.2 的黑色刷了一遍畫布,背景漆黑一片,如果說我們的需求是透明背景呢?

比如,我們要用這個夜景圖片做背景,然后在上面加上我們的流星,我們每一幀刷一層背景的小伎倆就用不了啦。因為我們要保證除開流星之外的部分,應該是透明的。

這里就要用到一個冷門的屬性了,globalCompositeOperation,全局組合操作? 原諒我放盪不羈的翻譯。
這個屬性其實就是用來定義后繪制的圖形與先繪制的圖形之間的組合顯示效果的。
他可以設置這些值

這些屬性說明沒必要仔細看,更不用記下來,直接看 api 示例 運行效果就很清楚了。示例里,先繪制的是填充正方形,后繪制的是填充圓形。

 
        

是不是豁然開朗,一目了然?

對於我們來說,原圖像是每一幀畫完的所有流星,目標圖像是畫完流星之后半透明覆蓋畫布的黑色矩形。而我們每一幀要保留的就是,上一幀 0.8 透明度的流星,覆蓋畫布黑色矩形我們不能顯示。

注意這里的 destination-out 和 destination-in,示例中這兩個屬性最終都只有部分源圖像保留了下來,符合我們只要保留流星的需求。我覺得 w3cschool 上描述的不是很正確,我用我自己的理解概括一下。

  • destination-in :只保留了源圖像(矩形)和目標圖像(圓)交集區域的源圖像
  • destination-out:只保留了源圖像(矩形)減去目標圖像(圓)之后區域的源圖像

上述示例目標圖像的透明度是 1,源圖像被減去的部分是完全不見了。而我們想要的是他可以按照目標透明度進行部分擦除。改一下示例里的代碼看看是否支持半透明的計算。

看來這個屬性支持半透明的計算。源圖像和目標圖像交疊的部分以半透明的形式保留了下來。也就是說如果我們要保留 0.8 透明度的流星,可以這樣設置 globalCompositeOperation

ctx.fillStyle = 'rgba(0,0,0,0.8)'
globalCompositeOperation = 'destination-in';
ctx.fillRect(0, 0, cvs.width, cvs.height);


// 或者
ctx.fillStyle = 'rgba(0,0,0,0.2)'
globalCompositeOperation = 'destination-out';
ctx.fillRect(0, 0, cvs.width, cvs.height);

最終效果

加上 globalCompositeOperation 之后的效果既最終效果:

 

github: https://github.com/gnauhca/dailyeffecttest/tree/master/b-meteorshower

快約上你的妹子看流星雨吧。

什么? 你沒有妹子?

轉載請注明出處:Web前端開發 » 撩妹技能 get,教你用 canvas 畫一場流星雨


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM