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

预览

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

粒子效果原理

在canvas中,能够经过getImageData()方法来获取像素数据。html

ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 1, 1);
const imageData = ctx.getImageData(0, 0, 1, 1);

imageData有三个属性:java

  • data:数组,包含了像素信息,每一个像素会有四个长度,如[255,0,0,255, ... ,255,127,0,255],分别表明该像素的RGBA值。
  • widthimageData对象的宽。
  • heightimageData对象的高。

首先在canvas写上某种颜色文字,再去分析像素数据(好比改像素是否有透明度等),而后本身记录下该像素点的位置git

下例是经过改变像素的数据而从新写出来的文字。程序员

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

这段代码只是示例说明一下,实际上才不会有人这么脑残去换颜色吧。github

获取点位置

要获取点的位置,首先要将字写在画布上,可是字又不能让别人看到。因此能够动态建立一个画布,这个画布不会append到任何节点上,只会用于写字。canvas

const cache = document.createElement('canvas');segmentfault

将宽高等与展现的画布设置成同样的。(不贴这部分的代码了)数组

建立一个对象,用于获取点的位置app

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

相关文章
相关标签/搜索