使用Pixi.js 画一颗闪耀的心心(粒子图片特效,以及拓展)

1.效果如下:

图片图片图片

图片图片图片

图片

2.需要素材 :一张心形图片,作为画粒子心形的模板
3.实现原理 :利用图片的像素信息,遍历图片的所有像素点,在自定义的规则下,满足条件的像素点位置画出小五角星,每一帧去刷新小五角星的属性,位置,大小,透明度等等。
4.代码解析:
创建代码:

 new dotText("dot-text", //绘制粒子Logo对应的canvasId
            {
                img_id: "dot-text-img",//绘制粒子使用的基础图片id
                text_id: false,//无用
                with_box: true,//是否额外生成白色随机粒子
                background_color: false,//背景颜色
                // dot_color: "#ff0000",//粒子的颜色,false默认为白色
                dot_color: false,
                mouse_hover: false,//为true,小球会被排斥成一个近似的圆弧形
                lighter: true//显示源图像 + 目标图像。
            });

每个变量的含义都有注释,通过调整不同变量的值,可以得到不同的效果
初始化代码

self.init = function () {//初始化
            var count = self.WIDTH >= 768 ? 12 : 8,//生成的随机粒子的个数
                maxR = self.WIDTH >= 768 ? 8 : 4;//粒子半径最大值
            (self.img || self.text) && self.drawText();//绘制粒子Logo,初始化为一个圆形
            if (self.options.with_box) {
                for (var i = 0; i < count; i++) {//随机飞的粒子
                    var  cell = new partical(self.WIDTH * Math.random(), self.HEIGHT * Math.random(), Math.random() * maxR, 30 * Math.random() + 20);
                    self.boxList.push(cell)
                }
            }
            self.imageCanvas.addEventListener("mousemove", self.mousemove);//添加鼠标移动事件
            self.imageCanvas.addEventListener("touchstart", self.touchmove);//添加触屏事件
            self.loop();//渲染循环
            window.removeEventListener("resize", self.resize);//添加resize事件
            window.addEventListener("resize", self.resize);//添加resize事件
        },

        self.init();

可以看出,有两部分粒子的初始化,一种是随机飞的粒子,一种是图片形状粒子,先看随机粒子的代码部分,由于很简单,不做分析.

var partical = function (x, y, r, s) {//随机粒子的构造函数
            this.x = x;//横坐标
            this.y = y;//纵坐标 
            this.r = r;//半径 
            this.s = s;//粒子的移动速度 
            this.rotation = Math.random() * Math.PI * 2;//旋转角度
        };

        partical.prototype = {
            constructor: partical,
            update: function () {//更新粒子的属性值
                this.rotation += .5 * Math.random() - .25;
                this.x += Math.cos(this.rotation) * this.s;
                this.y += Math.sin(this.rotation) * this.s;
                this.x > self.WIDTH ? this.x = 0 : this.x < 0 && (this.x = self.WIDTH);//超出屏幕则穿越到对面
                this.y > self.HEIGHT ? this.y = 0 : this.y < 0 && (this.y = self.HEIGHT);//超出屏幕则穿越到对面
            },
            render: function (ctx) {//重先渲染粒子
                var r = Math.floor(255 * Math.random()) + 1;
                var g = Math.floor(255 * Math.random()) + 1;
                var b = Math.floor(255 * Math.random()) + 1;
                var toRGB = "#" + Math.ceil(r).toString(16) + Math.ceil(g).toString(16) + Math.ceil(b).toString(16);
                ctx.save();
                ctx.fillStyle = toRGB;
                ctx.translate(this.x, this.y), 
                ctx.rotate(this.a);
                ctx.beginPath();
                ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
                ctx.fill();
                ctx.restore()
            }
        };

再看图片粒子的初始化代码

var cell = function (x, y, tx, ty, r) {
            this.x = x;//圆球的坐标x
            this.y = y; //圆球的坐标y
            this.tx = tx;//圆球最终会回到的坐标x 
            this.ty = ty; //圆球最终会回到的坐标y
            this.r = r;//半径,会变化
            this.br = r;//存储创建时候的半径,创建后,除非切屏,值就不会变 
            this.a = Math.random() * Math.PI * 2;//用来影响小球半径变化的一个弧度
            this.sx = .5 * Math.random();//x方向的速度
            this.sy = .5 * Math.random(); //y方向的速度
            this.alpha = 1 * Math.random();//圆球的alpha值
            this.delay = 80 * Math.random(); //多少帧后开始刷新属性,用于入场动画
            this.delayCtr = 0;//刷新控制器,如果delayCtr大于等于delay,则开始刷新属性
            self.options.dot_color && (this.color = self.options.dot_color);//Logo粒子的颜色 
            // this.mr = 10 * Math.random();//鼠标移动的时候,用来影响小球弹开的距离,值越大,弹得越开
            this.mr = 30 * Math.random();//鼠标移动的时候,用来影响小球弹开的距离,值越大,弹得越开
        };

再看粒子属性的刷新代码:

update: function () {
                if (this.delayCtr < this.delay) {//用于入场动画,this.delay帧后开始属性更新
                    this.delayCtr++
                    return;
                }
                var t = this.tx;//最终粒子的归宿位置x
                var e = this.ty;//最终粒子的归宿位置y
                this.a += .1,//小球半径变化的一个弧度值
                this.alpha += .05;//自定义的,增强闪烁效果
                if (this.alpha > 1) {
                    this.alpha = 0;
                } 
                //粒子归位的算法,这里用到的极限的思想
                this.x += (t - this.x) * this.sx;//由于sx不会为1,所以this.x永远不会等于t,也就是归宿位置x,但是由于每轮x都会像t靠近,所以最终位置会无限趋近归宿x,当经过帧数为无穷,横坐标就是为归宿x
                this.y += (e - this.y) * this.sy;//由于sy不会为1,所以this.y永远不会等于e,也就是归宿位置y,但是由于每轮x都会像e靠近,所以最终位置会无限趋近归宿y,当经过帧数为无穷,横坐标就是为归宿y
                this.r = this.br + Math.cos(this.a) * (.5 * this.br);//算小球的半径,范围在初始半径的0.5倍到1.5倍之间余弦波动
                var a = distance(this.x, this.y, self.mouse.x, self.mouse.y);//计算当前位置和鼠标位置的距离
                if (self.options.mouse_hover) {//吸附性强的算法
                    var n = Math.atan2(this.y - self.mouse.y, this.x - self.mouse.x),//鼠标和粒子当前位置的弧度值
                        r = 200 / a;//200 / 粒子当前位置和鼠标位置距离的
                    //最终粒子会被排斥成一个近似的圆弧形  
                    this.x += Math.cos(n) * r + .05 * (t - this.x);//最终里子会被弹向的位置x
                    this.y += Math.sin(n) * r + .05 * (e - this.y);//最终里子会被弹向的位置y
                } else {//吸附性弱的算法
                    var l = .04 * self.WIDTH;//l为屏幕宽度的4/100,
                    l < 30 && (l = 30);//l最小值取30
                    if (distance(this.tx, this.ty, self.mouse.x, self.mouse.y) < l) {//如果鼠标的位置和粒子的归宿位置距离小于l,
                        var d = this.tx > self.img_center_x ? this.mr : -this.mr,//粒子的归宿位置在图片中心位置的右侧,那么当粒子会被弹向右边,否在弹向左边
                            c = this.ty > self.img_center_y ? this.mr : -this.mr;//粒子的归宿位置在图片中心位置的上方,那么粒子会被弹向上方,否在弹向下方
                        this.x += (this.tx - self.mouse.x) / 10 + d;//最终里子会被弹向的位置x
                        this.y += (this.ty - self.mouse.y) / 10 + c;//最终里子会被弹向的位置y
                    }
                }
            },

每一行都有非常详细的注释。再看渲染部分代码:

render: function (imageCtx) {//每一帧根据小球的属性重先绘制
                imageCtx.save();
                imageCtx.globalAlpha = this.alpha; //圆球的alpha(0-1)随机
                abcimageCtx.fillStyle = this.color ? this.color : "#ffffff"; //圆球的颜色,默认为白色
                imageCtx.translate(this.x, this.y);//圆球移动到的位置 
                imageCtx.beginPath();
                //画一个半径为this.r的圆
                // imageCtx.arc(0, 0, this.r, 0, 2 * Math.PI);
                //画一个五角星
                var r1 = 3 * this.r / 2;
                var r2 = r1 / 2;
                var x1, x2, y1, y2;
                for (var i = 0; i < 5; i++) {
                    x1 = r1 * Math.cos((54 + i * 72) / 180 * Math.PI);
                    y1 = r1 * Math.sin((54 + i * 72) / 180 * Math.PI);
                    x2 = r2 * Math.cos((18 + i * 72) / 180 * Math.PI);
                    y2 = r2 * Math.sin((18 + i * 72) / 180 * Math.PI);
                    imageCtx.lineTo(x2, y2);
                    imageCtx.lineTo(x1, y1);
                }
                imageCtx.closePath();
                imageCtx.fill();
                imageCtx.restore();
            }

其实就是每一帧设置颜色,透明度,旋转,重先绘制五角星.再看看图片粒子的创建代码

self.drawText = function () {//根据给定Logo模板的像素点,绘制一些小圆球,然后把小圆球分布到一个圆周的位置
            var width = self.WIDTH,
                height = self.HEIGHT,
                txtCanvas = self.txtCanvas,
                txtCtx = self.txtCtx,
                img = self.img,
                dots = self.dots,//存储小球的数组
                maxR = width >= 768 ? 4 : 2,//球的半径最大值
                skip = width >= 768 ? self.skip : 5,//遍历像素的时候的间隔,就是每次遍历跳过多少个像素,默认是8个像素,如果显示宽度小于768,就一次跳过5个像素
                imageWidth, //Logo图片的宽度
                imageHeight, //Logo图片高度
                ox, //Logo图片的左上角x位置
                oy;//Logo图片左上角的y位置
                width >= 1600 && (skip = 12);
            if (img) {
                imageWidth = img.width;//Logo图片的宽度
                imageHeight = img.height; //Logo图片高度
                ox = img.getBoundingClientRect().left - self.imageCanvas.getBoundingClientRect().left;//Logo图片的左上角x位置
                oy = img.getBoundingClientRect().top - self.imageCanvas.getBoundingClientRect().top;//Logo图片左上角的y位置
                self.img_center_x = ox + imageWidth / 2;//Logo图片的中心位置x
                self.img_center_y = oy + imageHeight / 2; //Logo图片的中心位置y
                txtCanvas.width = width;//重新记录canvas的宽
                txtCanvas.height = height; //重新记录canvas的高
                txtCtx.globalCompositeOperation = "copy";//模式
                txtCtx.drawImage(self.img, ox, oy, imageWidth, imageHeight);//根据LOGO图片,用Canvas画了一份Logo图片的Image
            }
            self.imgData = txtCtx.getImageData(0, 0, width, height).data;//获取画出的Image的像素信息
            dots.splice(0, dots.length);//Logo粒子数组清空数据
            for (var y = 0; y < height; y += skip)//height为HEIGHT, skip为间隔的像素
                for (var x = 0; x < width; x += skip) {//width为WIDTH, skip为间隔的像素
                    var alphaIndex = 4 * (x + y * width) - 1;//根据像素信息获取对应像素点的alpha通道的index值
                    if (self.imgData[alphaIndex] > 0) {//如果alpha > 0的点就创建一个小球
                        var r = 2 * Math.PI * Math.random();//随机一个弧度
                        var dot = new cell(width / 2 + Math.cos(r) * width, height / 2 + Math.sin(r) * width, //圆心在self.WIDTH/2, self.HEIGHT/2,半径为self.WIDTH的圆弧上的点
                            x, //对应图片上像素的x
                            y, //对应图片上像素的y
                            Math.random() * maxR//球的半径
                        );
                        dots.push(dot)
                    }
                }
        }, 

可以看出,会根据图片的位置信息和像素信息创建出一组粒子,分布在一个半径为屏幕宽度一半的圆上.结合上面update的代码可以不停更新粒子的位置等属性。

最后通过了requestframe执行循环渲染

//每一帧执行一次loop,loop中再执行一次更新和绘制
        self.requestFrame = function (loop) {
            var e = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
            e ? self.anim_frame = e(loop) : self.anim_timer = window.setTimeout(loop, 50)
        }, 

再看看循环的代码

self.loop = function () {
            var dotList = self.dots,//存随机粒子的数组
                imageCtx = self.imageCtx,
                box = self.boxList,//存随机粒子的盒子
                width = self.WIDTH,//canvas宽
                height = self.HEIGHT,//canvas高
                background_color = self.options.background_color;
                imageCtx.globalCompositeOperation = "source-over";//默认。在目标图像上显示源图像。
            if (background_color) {//填充为纯色
                imageCtx.fillStyle = background_color;
                imageCtx.fillRect(0, 0, width, height);
            }
            else {
                //用纯色矩形刷新canvas画布
                imageCtx.fillStyle = "rgba(20, 20, 30, .5)";
                imageCtx.fillRect(0, 0, width, height);
                //用渐变矩形刷新canvas画布
                var l = imageCtx.createRadialGradient(width / 2, height / 10, 50, width / 2, height, height);
                l.addColorStop(0, "rgba(30, 50, 58, .5)");
                l.addColorStop(1, "rgba(20, 20, 30, .5)");
                imageCtx.fillStyle = l;
                imageCtx.fillRect(0, 0, width, height);
            }
            self.options.lighter && (imageCtx.globalCompositeOperation = "lighter")//如果lighter为真,那么用显示源加目标的形式显示
            if (self.img || self.text) {
                for (var i = 0, c = dotList.length; i < c; i++) {//每一帧重绘所有的Logo粒子
                    var h = dotList[i];
                    h.update(), 
                    h.render(imageCtx)
                }
            }
            if (self.options.with_box)
                for (var i = 0; i < box.length; i++) {//每一帧重绘所有的随机粒子
                    var p = box[i];
                    p.update();
                    p.render(imageCtx);
            }
            self.requestFrame(self.loop)//每一帧执行循环
        }, 

通过两个函数来回调用,整个渲染循环逻辑就建立了,于是出现了文章开头展示的效果。
5.思考拓展:
以上效果可以通过替换图片形成不同的粒子图片,可以通过配置参数,达到不同的展示效果。还可以通过给粒子再加一些属性,在update里按照自己想要的规则进行更新,在render里处理属性的渲染,可以打到很多不同的自定义效果。最后这是canvas画出的效果,其实完全也能用pixi实现。获取Pixi中sprite的像素信息,用Graphc画出一些小图形,然后在ticker里刷新属性,自定义render里进行重绘就可以达到一样的效果.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值