程序员的小浪漫----文字粒子效果

预览

完整项目预览----预览地址;

粒子效果原理

在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值。
  • widthimageData对象的宽。
  • heightimageData对象的高。

首先在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给整除,不然会造成获取坐标点错位的后果。

关于canvasmiddlecenter的规则:

this.ctx.font = 'bold 40px Arial';
this.ctx.fillText('你好',40 ,20);
复制代码

效果如下图所示

fillText设置的坐标点刚好会是整个字的中点,就是图中middlecenter的交点。其实以其它对齐方式也是可以的,看个人喜好。

更多的对齐规则参考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

你可能感兴趣的:(程序员的小浪漫----文字粒子效果)