html5 canvas 粒子特效

  不知不觉就已经好久没写过博客了,自从七月正式毕业后,离开了实习了将近九个月的老东家,进了鼠厂后,做的事都是比较传统的前端活,之前在tpy的时候只管做移动h5的特效以及小游戏,再加上实习所以时间比较充裕,canvas玩的比较多,而现在因为工作都是些传统前端工作,而且也忙,就基本上没再写过canvas相关的效果了。这个博客自己只是想分享一些自己做过的一些好玩的demo,所以正经的那些项目心得、插件什么的就基本上都不会放上来了。

  刚好昨天的时候闲了下来,就看了一下以前写的一些玩意,所以又想折腾下以前很喜欢折腾的粒子了。其实原理差不多,就是变着法子折腾,顺便自己也复习一下。这次的demo除了粒子运动之外,还加了鼠标的干涉。所以自己觉得还是有点搞头,所以就分享一下。

  先上个demo 效果:http://demo.wanghx.cn/particle/   ,表示不要再说什么在低版本IE上没效果之类的,这个是H5啊,同时最好在chrome上看,其他浏览器我都没测,纯碎为了好玩而做出来。有兴趣的可以把代码拷回去自己深究。

  图片或文字都可以分解成粒子。原理此前的博客都有说过,不过也再简单啰嗦一下,就是先将图片或者文字画在canvas上,然后通过画布对象的getImageData获取到画布上的所有像素点,也就是imageData对象的data数组,存放着画布的所有像素的rgba值。

  然后再遍历像素点,获取到当前像素点的rgba的a值也就是alpha透明度不为0,我直接舍弃了地透明度的,所以我写的判断是直接大于125就行了,255为不透明。更具体的原理可查看此前我的这个博文:随便谈谈用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中,所以就有了粒子一个一个出来的效果。

  在逐帧循环回调中,触发每个粒子对象的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来记录粒子初始的位置,当鼠标离开粒子时,重置粒子的目标位置。从而让粒子回到原来的位置。

  基本上整个的原理就这样,还是比较简单的,下面贴出全部代码,对这个有兴趣的可以拷贝回去自己修改出自己喜欢的效果:

技术分享
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta name="Keywords" content="猩"/>
    <meta name="Description" content="猩,猩猩,猩猩猩"/>
    <title></title>
    <link rel=‘stylesheet‘ href=‘/stylesheets/style.css‘/>
    <link rel="shortcut icon" href="/images/favicon.ico"/>
</head>
<body>
<canvas id="canvas">
    <img src="/images/orangutan.png"  id="logo"/>
</canvas>

<div class="btn-group">
    <a href="javascript:useImage()" class="btn image-btn">使用图片</a>
    <a href="javascript:useText(document.querySelector(‘.btn-group input‘).value)" class="btn text-btn">使用文字</a><input
            type="text" value="猩猩助手"/>
</div>

<script>
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext(‘2d‘);
var img;

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

var mouseX = null, mouseY = null;
var mouseRadius = 50;

var RAF = (function () {
    return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
        window.setTimeout(callback, 1000 / 60);
    };
})();

Array.prototype.forEach = function (callback) {
    for (var i = 0; i < this.length; i++) {
        callback.call((typeof this[i] === "object") ? this[i] : window, i, this[i]);
    }
};

window.onmousemove = function (e) {
    if (e.target.tagName == "CANVAS") {
        mouseX = e.clientX - e.target.getBoundingClientRect().left;
        mouseY = e.clientY - e.target.getBoundingClientRect().top;
    } else {
        mouseX = null;
        mouseY = null;
    }
};

var particleArray = [];
var animateArray = [];
var particleSize_x = 1;
var particleSize_y = 2;
var canvasHandle = {
    init: function () {
        this._reset();

        this._initImageData();

        this._execAnimate();
    },

    _reset: function () {
        particleArray.length = 0;
        animateArray.length = 0;

        this.ite = 30;
        this.start = 0;
        this.end = this.start + this.ite;
    },

    _initImageData: function () {
        this.imgx = (canvas.width - img.width) / 2;
        this.imgy = (canvas.height - img.height) / 2;


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

        ctx.drawImage(img, this.imgx, this.imgy, img.width, img.height);
        console.log(this.imgx)
        console.log(img.width)
        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();
                }
            }
        }

    },

    _execAnimate: function () {
        var that = this;

        var mouseSize = 10;

        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);
                })
            })
        }
    }
}

var timestamp , isrunning = false;
function animate(tick) {
    if (typeof tick == "function") {
        var newtime = new Date();
        var tickTime = timestamp ? ((newtime = new Date()) - timestamp) : 0;

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

        timestamp = newtime;

        tick(tickTime);

        RAF(function () {
            animate(tick)
        })
    }
}

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.static = 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;
            }
        }
    }
};

//use image
function useImage() {
    img = document.getElementById("logo");
    if (img.complete) {
        canvasHandle.init();
    } else {
        img.onload = function () {
            canvasHandle.init();
        }
    }
}

//use text
function useText(text) {
    img = document.createElement(‘canvas‘);
    img.width = 600;
    img.height = 180;
    var imgctx = img.getContext("2d");
    imgctx.textAlign = "center";
    imgctx.textBaseline = "middle";
    imgctx.font = "100px 微软雅黑";
    imgctx.fillText(text || ‘猩猩助手‘, img.width / 2, img.height / 2);
    canvasHandle.init();
}

useImage()
</script>
</body>
</html>
View Code

  

  

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。