用canvas寫一個(gè)粒子動(dòng)畫

本文章適合于有一定canvas畫圖經(jīng)驗(yàn)的人閱讀,能夠熟悉基本的繪圖API,廢話不多說,下面就開始了。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>

    <style type="text/css">
        html,
        body {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }
        .canvasview {
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
    </style>
</head>
<body>
<div class="canvasview"></div>
</body>

<script type="text/javascript" src="granule.js" ></script>
<script>

    var setting = {
        "el": ".canvasview",        // (必選)指定一個(gè)父容器
        "bgcolor": "#222222",       // (可選)畫布的背景顏色
        "color": "#ffffff",         // (可選)粒子的顏色
        "concentration": 0.2,       // (可選)濃度
        "radius": 5,                // (可選)例子半徑
        "opacity": 0.9,             // (可選)粒子透明度
        "duration": 16,             // (可選)運(yùn)動(dòng)的時(shí)間(秒)大概值不一定精確
        "rangeRadius": 512,         // (可選)粒子運(yùn)動(dòng)的范圍

    }

    var test = new granule(setting);    // 設(shè)置動(dòng)畫的屬性
    test.startAnimation();              // 啟動(dòng),開始運(yùn)行

</script>
</html>

上面是html代碼,有一個(gè)canvasView的大容器,等下我們會(huì)在這里繪制canvas。
setting里面是一些比較總要的參數(shù),包括粒子濃度,運(yùn)動(dòng)范圍,背景顏色等。

首先第一步,寫好入口文件

var granule = function(setting) {
    this.TWEEN = ["easeInOutQuad", "easeInOutCubic", "easeInOutBack"];
    this.sett = setting
    this._init();
    this.randomCreateAllBall();
}

/**
 * 用于初始化
 * 
 * 創(chuàng)建畫布: this.canvas;
 * 畫圖環(huán)境:this.cc
 * 計(jì)算寬高:this.cw, this.ch
 * 圓的半徑: this.radius , 默認(rèn)5px
 * 例子的濃度: this.concentration
 * 粒子的運(yùn)動(dòng)時(shí)間: this.duration
 * 粒子的 活動(dòng)范圍半徑:this.rangeRadius
 * 計(jì)算粒子數(shù)量: this.cg
 */
granule.prototype._init = function() {
    var el = document.querySelector(this.sett.el);
    var elbox = el.getBoundingClientRect();
        this.canvas = document.createElement("canvas");
    this.cc = this.canvas.getContext("2d");
    this.cw = parseInt(this.sett.width) || elbox.width;
    this.ch = parseInt(this.sett.height) || elbox.height;
    this.radius = this.sett.radius || 5;
    this.opacity = this.sett.opacity || 1;
    this.concentration = this.sett.concentration || 1;
    this.duration = this.sett.duration || 8;
    this.rangeRadius = this.sett.rangeRadius || (this.cw + this.ch) / 4;
    this.tweenType = this.sett.tweenType || "";
    this.tweenAni = this.sett.tweenAni || "easeInOutCubic";
        this.cg = Math.floor(this.cw * this.ch / (this.radius * this.radius * 8 * 8) * this.concentration);
    this.color = this.sett.color || "#ffffff";
    this.bgcolor = this.sett.bgcolor || "#222222";
        this.canvas.width = this.cw;
    this.canvas.height = this.ch;
    el.appendChild(this.canvas);
}

1.this.rangeRadius 是粒子運(yùn)動(dòng)的幅度大小
2.this.cd 是運(yùn)動(dòng)的粒子數(shù)量
如何保證粒子不間斷的運(yùn)動(dòng)? 需要給每個(gè)粒子設(shè)定一個(gè)運(yùn)動(dòng)的范圍,運(yùn)動(dòng)的形式可以用緩動(dòng)函數(shù),下面會(huì)給出來。在還沒到達(dá)這個(gè)運(yùn)動(dòng)終點(diǎn)時(shí),需要不停的重繪,當(dāng)?shù)竭_(dá)時(shí)有需要給這個(gè)粒子重新設(shè)置一個(gè)運(yùn)動(dòng)的軌跡,大概原理就是這樣,先看一下如何畫出所有粒子。

granule.prototype.randomCreateAllBall = function() {
    var all = [];
    for(var i = 0; i < this.cg; i++) {
        var ball = {};
        ball.stauts = "0"; // 增加一個(gè)特殊狀態(tài),這個(gè)狀態(tài)表示該粒子目前時(shí)候處于 [自定義動(dòng)畫中](自定義動(dòng)畫:輸入指令而顯示的動(dòng)畫)。0:表示目前沒有自定義動(dòng)畫
        ball.color = this.color;

        ball.r = this.radius;
        ball.x = this.rdmmm(0, this.cw); // 實(shí)際坐標(biāo)x, 對(duì)應(yīng)tween中的b:起點(diǎn)
        ball.y = this.rdmmm(0, this.ch); // 實(shí)際坐標(biāo)y,

        ball.txb = ball.x; // 對(duì)應(yīng)tween中的b:起點(diǎn)
        ball.tyb = ball.y; // 起點(diǎn), 對(duì)應(yīng)y坐標(biāo)

        ball.txc = this.rdmmm(-this.rangeRadius, this.rangeRadius); // 對(duì)應(yīng)tween中的c:位移(可以隨機(jī)一個(gè))
        ball.tyc = this.rdmmm(-this.rangeRadius, this.rangeRadius);

        ball.td = (this.duration + this.rdmmm(-this.duration / 2, this.duration / 2)) * 60; // 對(duì)應(yīng)tween中的d:終止時(shí)間,建議設(shè)置為60的倍數(shù)
        ball.tt = this.rdmmm(0, ball.td); // 對(duì)應(yīng)tween中的t:時(shí)間  注意這里的時(shí)間不是從0開始 意味著一開始繪制的時(shí)候粒子的坐標(biāo)并不是剛生成時(shí)的坐標(biāo)

        if(this.tweenType) {
            ball.tp = this.tweenType
        } else {
            ball.tp = this.TWEEN[this.rdmmm(0, this.TWEEN.length)];
                     //每一粒子的運(yùn)動(dòng)函數(shù)都是隨機(jī)的
        }

        all.push(ball);
    }
    this.balls = all;
}

/**
 * 生成指定范圍的隨機(jī)數(shù)
 * @param {int} min: 隨機(jī)數(shù)的下限
 * @param {int} max: 隨機(jī)數(shù)的上限
 */
granule.prototype.rdmmm = function(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}

為了方便理解上面代碼,看一下給出的運(yùn)動(dòng)函數(shù)

/**
this.TWEEN = ["easeInOutQuad", "easeInOutCubic", "easeInOutBack"];
 * 二次緩動(dòng), 具體請(qǐng)查看Tween源碼
 * @param {int} t:當(dāng)前時(shí)間
 * @param {int} b:初始值
 * @param {int} c:變化量, 位移
 * @param {int} d:持續(xù)時(shí)間
 */
granule.prototype.easeInOutQuad = function(t, b, c, d) {
    if((t /= d / 2) < 1) return c / 2 * t * t + b;
    return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
// 三次緩動(dòng)
granule.prototype.easeInOutCubic = function(t, b, c, d) {
    if((t /= d / 2) < 1) return c / 2 * t * t * t + b;
    return c / 2 * ((t -= 2) * t * t + 2) + b;
}
// bakc
granule.prototype.easeInOutBack = function(t, b, c, d, s) {
    if(s == undefined) s = 1.70158;
    if((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
    return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
}
// 彈簧效果
granule.prototype.easeOutBounce = function(t, b, c, d) {
    if((t /= d) < (1 / 2.75)) {
        return c * (7.5625 * t * t) + b;
    } else if(t < (2 / 2.75)) {
        return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
    } else if(t < (2.5 / 2.75)) {
        return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
    } else {
        return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
    }
}

我來介紹一下function(t,b,c,d)里面的參數(shù)
t:動(dòng)畫開始時(shí)間 b:動(dòng)畫開始位置 c:動(dòng)畫結(jié)束位置 d:動(dòng)畫持續(xù)時(shí)間
因此我們可以通過給t設(shè)定一個(gè)值,得到他該時(shí)間點(diǎn)上的具體位置。

好了,所有的粒子都已經(jīng)畫出來了,運(yùn)動(dòng)軌跡也已經(jīng)設(shè)置好,接下來就是每一幀的重繪了。

granule.prototype.updataBallPosition = function(b) {
    // 根據(jù) tween 計(jì)算在當(dāng)前幀的位置
    var newx = this[b.tp](b.tt, b.txb, b.txc, b.td);
    var newy = this[b.tp](b.tt, b.tyb, b.tyc, b.td);

    // 四個(gè)方向的碰撞檢測(cè)
    if(newx < 0) {
        newx = -newx;
    }
    if(newy < 0) {
        newy = -newy;
    }
    if(newx > this.cw) {
        newx = 2 * this.cw - newx;
    }
    if(newy > this.ch) {
        newy = 2 * this.ch - newy;
    }

    b.x = newx;
    b.y = newy;

    // 當(dāng)運(yùn)動(dòng)時(shí)間結(jié)束之后   普通的粒子重新隨機(jī)一個(gè)位移
    if(b.stauts == 0) {
        if(++b.tt >= b.td) {
            b.txb = b.x;
            b.tyb = b.y;
            b.txc = this.rdmmm(-this.rangeRadius, this.rangeRadius);
            b.tyc = this.rdmmm(-this.rangeRadius, this.rangeRadius);
            b.tt = 0;
        }
    } 
    
    return b;
}

當(dāng)b.tt >= b.td 也就是該粒子運(yùn)動(dòng)的時(shí)間到了之后,需要重新給他設(shè)置一下。
最后就是要啟動(dòng)該動(dòng)畫

granule.prototype.startAnimation = function() {
    var self = this;
    var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

    start();

    function start() {
        for(var i = 0; i < self.balls.length; i++) {
            var ball = self.balls[i];
            var newb = self.updataBallPosition(ball);
            self.balls[i] = newb;
        }
        self.drawBalls();
        raf(start);
    }
}

/**
 * 繪制所有的小球
 */
granule.prototype.drawBalls = function() {
    this.drawbg();
    this.cc.globalAlpha = this.opacity;
       //基本畫圓Api
    for(var i = 0; i < this.balls.length; i++) {
        var ball = this.balls[i];
        this.cc.beginPath();
        this.cc.fillStyle = ball.color || this.color;
        this.cc.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
        this.cc.closePath();
        this.cc.fill();
    }
}

如果對(duì)requestAnimationFrame不熟悉的同學(xué)自行g(shù)oogle。在每一幀調(diào)用self.updataBallPosition(ball)計(jì)算出該求得位置,self.drawBalls()畫出該球。

好了,到這里也還差不多結(jié)束了,需要思考的是小球的運(yùn)動(dòng)軌跡改變和每一幀的重繪,當(dāng)碰撞后怎么辦。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容