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里进行重绘就可以达到一样的效果.