html5 canvas 粒子特效

不知不覺就已經很久沒寫過博客了,自從七月正式畢業後,離開了實習了將近九個月的老東家,進了鼠廠後,作的事都是比較傳統的前端活,以前在tpy的時候只管作移動h5的特效以及小遊戲,再加上實習因此時間比較充裕,canvas玩的比較多,而如今由於工做都是些傳統前端工做,並且也忙,就基本上沒再寫過canvas相關的效果了。這個博客本身只是想分享一些本身作過的一些好玩的demo,因此正經的那些項目心得、插件什麼的就基本上都不會放上來了。html

  恰好昨天的時候閒了下來,就看了一下之前寫的一些玩意,因此又想折騰下之前很喜歡折騰的粒子了。其實原理差很少,就是變着法子折騰,順便本身也複習一下。此次的demo除了粒子運動以外,還加了鼠標的干涉。因此本身以爲仍是有點搞頭,因此就分享一下。前端

  先上個demo 效果:http://whxaxes.github.io/canvas-test/src/Particle-demo/orangutan/index.html   ,表示不要再說什麼在低版本IE上沒效果之類的,這個是H5啊,同時最好在chrome上看,其餘瀏覽器我都沒測,純碎爲了好玩而作出來。有興趣的能夠把代碼拷回去本身深究。git

  圖片或文字均可以分解成粒子。原理此前的博客都有說過,不過也再簡單囉嗦一下,就是先將圖片或者文字畫在canvas上,而後經過畫布對象的getImageData獲取到畫布上的全部像素點,也就是imageData對象的data數組,存放着畫布的全部像素的rgba值。github

  而後再遍歷像素點,獲取到當前像素點的rgba的a值也就是alpha透明度不爲0,我直接捨棄了地透明度的,因此我寫的判斷是直接大於125就好了,255爲不透明。更具體的原理可查看此前個人這個博文:隨便談談用canvas來實現文字圖片粒子化chrome

  獲取到粒子的位置後,就實例化出粒子對象,代碼以下:canvas

複製代碼
ctx.drawImage(img, this.imgx, this.imgy, img.width, img.height);
        var imgData = ctx.getImageData(this.imgx, this.imgy, img.width, img.height);

        for (var x = 0; x < img.width; x += particleSize_x) {
            for (var y = 0; y < img.height; y += particleSize_y) {
                var i = (y * imgData.width + x) * 4;

                if (imgData.data[i + 3] >= 125) {
                    var color = "rgba(" + imgData.data[i] + "," + imgData.data[i + 1] + "," + imgData.data[i + 2] + "," + imgData.data[i + 3] + ")";

                    var x_random = x + Math.random() * 20,
                            vx = -Math.random() * 200 + 400,
                            y_random = img.height/2 - Math.random() * 40 + 20,
                            vy;

                    if (y_random < this.imgy + img.height / 2) {
                        vy = Math.random() * 300;
                    } else {
                        vy = -Math.random() * 300;
                    }

                    particleArray.push(new Particle(x_random + this.imgx,y_random + this.imgy,x + this.imgx,y + this.imgy,vx,vy,color));

                    particleArray[particleArray.length - 1].drawSelf();
                }
            }
        }
複製代碼

  將實例化的粒子對象扔進一個數組裏保存起來。而後執行動畫循環。 數組

複製代碼
particleArray.sort(function (a, b) {
            return a.ex - b.ex;
        });

        if (!this.isInit) {
            this.isInit = true;
            animate(function (tickTime) {
                if (animateArray.length < particleArray.length) {
                    if (that.end > (particleArray.length - 1)) {
                        that.end = (particleArray.length - 1)
                    }
                    animateArray = animateArray.concat(particleArray.slice(that.start, that.end))

                    that.start += that.ite;
                    that.end += that.ite;
                }

                animateArray.forEach(function (i) {
                    this.update(tickTime);
                })
            })
        }
複製代碼

  animate方法的回調即爲每次畫布逐幀循環時調用的方法,其中animateArray就是真正用於放置於循環舞臺的粒子對象,也就是上面demo中看到的從左到右一個一個粒子出現的效果,其實就是從particleArray中取粒子對象,在每一幀中扔幾十個進animateArray中,因此就有了粒子一個一個出來的效果。瀏覽器

  animate方法代碼以下:緩存

複製代碼
var tickTime = 16;
function animate(tick) {
    if (typeof tick == "function") {
        var tickTime = 16;

        ctx.clearRect(0, 0, canvas.width, canvas.height);

        tick(tickTime);

        RAF(function () {
            animate(tick)
        })
    }
}
複製代碼

  這個代碼就比較簡單了,設置每一幀之間的時間差,我通常是設成16毫秒,這個就本身看哈,給tick方法傳參循環。dom

  在逐幀循環回調中,觸發每一個粒子對象的update,其中粒子的運動函數,繪畫函數所有會由update函數觸發。

  下面這個是粒子對象的封裝,其中x,y爲粒子的位置,ex,ey爲粒子的目標位置,vx,vy爲粒子的速度,color爲粒子的顏色,particleSize爲粒子的大小,stop是粒子是否靜止,maxCheckTimes和checkLength和checkTimes是檢測粒子是否靜止的屬性,由於粒子在運動的時候,位置是無時無刻都在變化,因此是沒有絕對靜止的,因此須要手動檢測是否約等於靜止,而後再給予粒子靜止狀態,當粒子與目標位置的距離小於checkLength,而且在連續10幀的檢測都粒子與距離目標都是小於checkLength,則說明粒子約等於靜止了,將粒子的stop屬性置爲true,再接下來的動畫逐幀循環中,對於stop爲true的粒子則不進行運動計算:

複製代碼
function Particle(x, y, ex, ey, vx, vy, color) {
    this.x = x;
    this.y = y;
    this.ex = ex;
    this.ey = ey;
    this.vx = vx;
    this.vy = vy;
    this.a = 1500;
    this.color = color;
    this.width = particleSize_x;
    this.height = particleSize_y;

    this.stop = false;this.maxCheckTimes = 10;
    this.checkLength = 5;
    this.checkTimes = 0;
}


var oldColor = "";
Particle.prototype = {
    constructor: Particle,

    drawSelf: function () {
        if (oldColor != this.color) {
            ctx.fillStyle = this.color;
            oldColor = this.color
        }

        ctx.fillRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
    },

    update: function (tickTime) {
        if (this.stop) {
            this.x = this.ex;
            this.y = this.ey;
        } else {
            tickTime = tickTime / 1000;
            var cx = this.ex - this.x;
            var cy = this.ey - this.y;
            var angle = Math.atan(cy / cx);
            var ax = Math.abs(this.a * Math.cos(angle));
            ax = this.x > this.ex ? -ax : ax

            var ay = Math.abs(this.a * Math.sin(angle));
            ay = this.y > this.ey ? -ay : ay;

            this.vx += ax * tickTime;
            this.vy += ay * tickTime;
            this.vx = ~~this.vx * 0.95;
            this.vy = ~~this.vy * 0.95;
            this.x += this.vx * tickTime;
            this.y += this.vy * tickTime;

            if (Math.abs(this.x - this.ex) <= this.checkLength && Math.abs(this.y - this.ey) <= this.checkLength) {
                this.checkTimes++;
                if (this.checkTimes >= this.maxCheckTimes) {
                    this.stop = true;
                }
            } else {
                this.checkTimes = 0
            }
        }

        this.drawSelf();

        this._checkMouse();
    },

    _checkMouse: function () {
        if (!mouseX) {
            if (this.recordX) {
                this.stop = false;
                this.checkTimes = 0;

                this.a = 1500;
                this.ex = this.recordX;
                this.ey = this.recordY;

                this.recordX = null;
                this.recordY = null;
            }
            return;
        }

        var distance = Math.sqrt(Math.pow(mouseX - this.x, 2) + Math.pow(mouseY - this.y, 2));
        var angle = Math.atan((mouseY - this.y) / (mouseX - this.x));
        if (distance < mouseRadius) {
            this.stop = false;
            this.checkTimes = 0;

            if (!this.recordX) {
                this.recordX = this.ex;
                this.recordY = this.ey;
            }

            this.a = 2000;

            var xc = Math.abs((mouseRadius - distance) * Math.cos(angle));
            var yc = Math.abs((mouseRadius - distance) * Math.sin(angle));
            xc = mouseX > this.x ? -xc : xc;
            yc = mouseY > this.y ? -yc : yc;
            this.ex = this.x + xc;
            this.ey = this.y + yc;
        } else {
            if (this.recordX) {
                this.stop = false;
                this.checkTimes = 0;

                this.a = 1500;
                this.ex = this.recordX;
                this.ey = this.recordY;

                this.recordX = null;
                this.recordY = null;
            }
        }
    }
};
複製代碼

  粒子的方法中,drawself爲粒子的繪製自身的方法,畫布的繪製對象的方法的調用次數越少,對整個動畫的性能提高越大。所以,把粒子畫成正方形,由於畫正方形只需調用一個fillRect方法,而若是畫圓形則須要先調用beginPath開始路徑的繪製,再調用arc繪製路徑,最後再經過fill填充顏色。性能方面確定是畫正方形性能更好,因而直接用fillRect。而也對粒子的color進行緩存,若是連續繪製的多個粒子顏色相同,就不用重複調用fillStyle方法更新畫筆顏色。

  而後是update方法,這個方法是粒子運動的核心,可是原理很簡單,就是一些簡單的運動學知識,獲取到粒子與目標點夾角的角度,經過角度將粒子的加速度分解爲水平和垂直加速度,再計算出粒子在新的一幀的水平速度和垂直速度,而後再經過新的速度計算出粒子新的位置,最後再繪製出來。update方法底部的if else則是判斷粒子是否靜止的代碼。

  粒子的最後一個方法,checkmouse其實就是檢測鼠標位置,若是粒子跟鼠標的距離小於15,則將粒子的目標位置置於與鼠標距離爲15的地方,爲了保證鼠標移開後粒子還能回到原來的地方,因此用了個recordX和recordY來記錄粒子初始的位置,當鼠標離開粒子時,重置粒子的目標位置。從而讓粒子回到原來的位置。

  基本上整個的原理就這樣,代碼有時會有所更新,若要最新源碼,請直接訪問github地址:

  https://github.com/whxaxes/canvas-test/tree/gh-pages/src/Particle-demo/orangutan

相關文章
相關標籤/搜索