预览
完整项目预览----预览地址;
粒子效果原理
在canvas中,可以通过getImageData()
方法来获取像素数据。
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 1, 1);
const imageData = ctx.getImageData(0, 0, 1, 1);
复制代码
imageData
有三个属性:
data
:数组,包含了像素信息,每个像素会有四个长度,如[255,0,0,255, ... ,255,127,0,255]
,分别代表该像素的RGBA值。width
:imageData
对象的宽。height
:imageData
对象的高。
首先在canvas
上写上某种颜色文字,再去分析像素数据(比如改像素是否有透明度等),然后自己记录下该像素点的位置。
下例是通过改变像素的数据而重新写出来的文字。
ctx.font = 'bold 40px Arial';
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText('你好啊', 60, 20);
document.querySelector('#button').addEventListener('click', function(){
const imgData = ctx.getImageData(0, 0, 120, 40);
for(let i = 0;i < imgData.data.length; i+=4){
if(imgData.data[i + 3] == 0) continue;
imgData.data[i] = 255;
imgData.data[i + 1] = 0;
imgData.data[i + 2] = 0;
// imgData.data[i + 3] = 255; 这个代表的是透明度 透明度不变 255最高 0最低
}
ctx.putImageData(imgData,120,0);
});
复制代码
这段代码只是示例说明一下,实际上才不会有人这么脑残去换颜色吧。
获取点位置
要获取点的位置,首先要将字写在画布上,但是字又不能让别人看到。所以可以动态创建一个画布,这个画布不会append
到任何节点上,只会用于写字。
const cache = document.createElement('canvas');
将宽高等与展示的画布设置成一样的。(不贴这部分的代码了)
创建一个对象,用于获取点的位置
const ShapeBuilder = {
//初始化字的对齐方式等,我认为middle 与 center比较好计算一点
init(width, height){
this.width = width;
this.height = height;
this.ctx = cache.getContext('2d');
this.ctx.textBaseline = 'middle';
this.ctx.textAlign = 'center';
},
//获取位置之前必须先要写入文字。 这里的size=40是默认值
write(words, x, y, size = 40){
//清除之前写的字。
this.ctx.clearRect(0, 0, this.width, this.height);
this.font = `bold ${size}px Arial`;
this.ctx.fillText(words, x, y);
//记录当前文字的位置,方便计算获取像素的区域
this.x = x;
this.y = y;
this.size = size;
this.length = words.length;
},
getPositions(){
//因为imgData数据非常的大,所以尽可能的缩小获取数据的范围。
const xStart = this.x - (this.length / 2) * this.size,
xEnd = this.x + (this.length / 2) * this.size,
yStart = this.y - this.size / 2,
yEnd = this.y + this.size / 2,
//getImageData(起点x, 起点y, 宽度, 高度);
data = this.ctx.getImageData(xStart, yStart, this.size * this.length, this.size).data;
//间隔 (下面有介绍)
const gap = 4;
let positions = [], x = xStart, y = yStart;
for(var i = 0;i < data.length; i += 4 * gap){
if(data[i+3] > 0){
positions.push({x, y});
}
x += gap;
if(x >= xEnd){
x = xStart;
y += gap;
i += (gap - 1) * 4 * (xEnd - xStart);
}
}
return positions;
}
}
ShapeBuilder.init();
复制代码
关于gap
:在循环imgData
数组的时候,数据量太大可能会造成卡顿,所以可以使用间隔来获取坐标点的方法。不过可能会造成文字部分地方缺失。就需要个人来权衡利弊,自己来调整了。
gap
的值必须能被xEnd-xStart
给整除,不然会造成获取坐标点错位的后果。
关于canvas
中middle
与center
的规则:
this.ctx.font = 'bold 40px Arial';
this.ctx.fillText('你好',40 ,20);
复制代码
效果如下图所示
fillText
设置的坐标点刚好会是整个字的中点,就是图中middle
与center
的交点。其实以其它对齐方式也是可以的,看个人喜好。
更多的对齐规则参考HTML 5 Canvas 参考手册的文本。
创建微粒类
微粒应该随机生成,然后移动到指定的位置去。
微粒类的属性:
自身当前位置(x,y), 目标位置:(xEnd,yEnd),自身大小(size),自身颜色(color),移动快慢(e)
方法:go()
:每一帧都要移动一段距离,render()
:渲染出微粒(我用心形的形状)
class Particle {
constructor({x, y, size = 2, color, xEnd, yEnd, e = 60} = {}){
this.x = x;
this.y = y;
this.size = size;
this.color = color || `hsla(${Math.random() * 360}, 90%, 65%, 1)`;
this.xEnd = xEnd;
this.yEnd = yEnd;
//经过e帧之后到达目标地点
this.e = e;
//计算每一帧走过的距离
this.dx = (xEnd - x) / e;
this.dy = (yEnd - y) / e;
}
go(){
//到目的后保持不动 (其实这里也可以搞点事情的)
if(--this.e <= 0) {
this.x = this.xEnd;
this.y = this.yEnd;
return ;
}
this.x += this.dx;
this.y += this.dy;
}
render(ctx){
this.go();
//下面是画出心型的贝塞尔曲线
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.moveTo(this.x + 0.5 * this.size, this.y + 0.3 * this.size);
ctx.bezierCurveTo(this.x + 0.1 * this.size, this.y, this.x,
this.y + 0.6 * this.size, this.x + 0.5 *
this.size, this.y + 0.9 * this.size);
ctx.bezierCurveTo(this.x + 1 * this.size, this.y + 0.6 *
this.size, this.x + 0.9 * this.size, this.y,
this.x + 0.5 * this.size,
this.y + 0.3 * this.size);
ctx.closePath();
ctx.fill();
return true;
}
}
复制代码
微粒类最基本的属性与方法就是这些,如果要让粒子更好看一点,或者更生动一点,可以自己添加一些属性与方法。
具体流程
const canvas = {
init(){
//设置一些属性
this.setProperty();
//创建微粒
this.createParticles();
//canvas的循环
this.loop();
},
setProperty(){
this.ctx = studio.getContext('2d');
this.width = document.body.clientWidth;
this.height = document.body.clientHeight;
this.particles = [];
},
createParticles(){
let dots;
//ShapeBuilder.write(words, x, y, size)
ShapeBuilder.write('每个字都是',this.width / 2, this.height / 3, 120);
dots = ShapeBuilder.getPositions(6);
ShapeBuilder.write('爱你的模样', this.width / 2, this.height * 2 / 3, 120);
dots = dots.concat(ShapeBuilder.getPositions(6));
//dots已经获取到了字的坐标点
//每一个微粒的目标地点都是dots的坐标
//每一个微粒都随机出生在画布的某个位置
for(let i = 0; i < dots.length; i++){
this.particles.push(new Particle({
xEnd:dots[i].x,
yEnd:dots[i].y ,
x: Math.random() * this.width,
y: Math.random() * this.height,
size:6,
color:'hsla(360, 90%, 65%, 1)'
}));
}
},
loop(){
//每一帧清除画布,然后再渲染微粒就可以了
requestAnimationFrame(this.loop.bind(this));
this.ctx.clearRect(0, 0, this.width, this.height);
for(var i = 0; i < this.particles.length; i++){
this.particles[i].render(this.ctx);
}
}
}
canvas.init();
复制代码
如果想要给每个粒子加上小尾巴的话,那么在每一帧的时候,就不要清除画布,而且覆盖一层有透明度的底色。
//修改loop方法
//this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.fillStyle = 'rgba(0,0,0,0.2)';
this.ctx.fillRect(0, 0, this.width, this.height);
复制代码
这样的话会变成如下效果
最后
在这这篇文章的时候,并没有注意太多细节,比如gap
应该是可以被设置的,或者是一个被特殊标注的常量,而不应该随便写在方法中。对于本例的代码,切勿生搬硬套,重要的是要理解原理,以及自己亲自动手尝试。
我也是在写这篇文章的过程中,才发现了之前获取position
一个不精准的地方。
这里只讲了粒子效果最基础的用法,实际上还可以做出很多非常炫酷的效果
比如在粒子到达目的地后还可以抖动什么的
粒子形状、颜色的变化等等。
这个项目还可以搞很多事情的,大家也可以自己多来尝试弄些更加炫酷的效果。
烟花效果可以看一下我的上一篇,程序员的小浪漫----烟火
完整项目
github项目地址
如果觉得还不错,请star一个吧。
参考项目
github上的一个项目---- shape-shifter
这个项目我觉得非常不错,可惜作者都消失好多年了。
codepen.io 上的一个作品 ---- Love In Hearts